Update Umsetzung

Marcel Schwarz 2021-01-26 18:51:59 +00:00
parent 5a5ad1ad9e
commit 15150d4ed8

@ -1,9 +1,9 @@
# Backend
## Datenbank
Die Entscheidung welche Datenbank benutzt werden sollte war relativ einfach, sie sollte vor allem leichtgewichtig sein, schnell und einfach zu bedienen. Aufgrund der hervorragenden Integration von SQLite in Python haben wir uns auch für SQLite entschieden. Sie bietet genug Datentypen, um alle unsere Anforderungen abzudecken.
Die Entscheidung welche Datenbank benutzt werden sollte war relativ einfach, sie sollte vor allem leichtgewichtig sein, schnell und einfach zu bedienen. Aufgrund der hervorragenden Integration von SQLite in Python haben wir uns letztendlich auch für SQLite entschieden. Sie bietet genug Datentypen, um alle unsere Anforderungen abzudecken.
Zunächst war es wichtig, zu erkennen, welche Tabellen nötig waren. Hier sind zunächst die Tabellen zu nennen, die die reine Datenhaltung der OpenApi Data abbilden.
Zunächst war es wichtig, zu erkennen, welche Tabellen nötig waren. Hier sind als erstes die Tabellen zu nennen, die die reine Datenhaltung der OpenApi Daten abbilden.
#### usage_stats
```sql
CREATE TABLE IF NOT EXISTS usage_stats(
@ -30,7 +30,7 @@ CREATE TABLE IF NOT EXISTS bike_points(
id_num INTEGER
)
```
Diese Tabelle spiegelt lediglich die Online Bikepoints einmalig bei der Initialisierung der Datenbank. Sie wird benutzt um Namen und ID mit der usage_stats Tabelle zu vergleichen.
Diese Tabelle spiegelt lediglich die online Bikepoints einmalig bei der Initialisierung der Datenbank. Sie wird benutzt um Namen und IDs mit der usage_stats Tabelle zu vereinen.
#### accidents
```sql
@ -139,7 +139,7 @@ Hier ist zu sehen, wie auf einem Subrouter über eine Annotation (ähnlich wie b
Der Aufruf von `api_database.get_all_accidents()` ist nun nur noch eine reine delegation an eine Funtkion mit den SQL Befehlen.
### Probleme
Zunächst sah alles gut aus und die API war auch entsprechend schnell. Sobald allerdings größere Zeitspannen abgerufen werden, wuchs die Zeit enorm. Folgende Werte wurden im zusammenhang mit einer Bikestation gemessen:
Zunächst sah alles gut aus und die API war auch entsprechend schnell. Sobald allerdings größere Zeitspannen abgerufen werden, wuchs die Zeit enorm. Folgende Werte wurden im Zusammenhang mit **einer Bikestation** gemessen:
| Abrufzeitraum | Durchschnittliche Zeit in Sekunden |
| --- | --- |
| ein Tag | 0.3 |
@ -152,19 +152,19 @@ Zunächst sah alles gut aus und die API war auch entsprechend schnell. Sobald al
Das würde bedeuten, dass wir nur etwa 10% unseres Datensatzes nutzen könnten.
## Performance
Um die oben genannten Probleme zu beheben wurde enormer Aufwand betrieben! Es wurde mit erweterten Datenbankfeatures getesten und sogar das Datenbank System gewechselt. Nichts half auf anhieb. Es blieb nur noch eine Möglichkeit, das kluge Vorberechnen von Werten.
Um die oben genannten Probleme zu beheben wurde enormer Aufwand betrieben! Es wurde mit erweterten Datenbankfeatures getestet und sogar das Datenbank System gewechselt. Nichts half auf anhieb. Es blieb nur noch eine Möglichkeit, das kluge Vorberechnen von Werten.
Schnell stelle sich heraus, dass der Datumvergleich sehr lange dauert. Es wurde viel Zeit damit verbracht, den Timestamp von einem Integer in ein Datum zu konvertieren. Ebenso war das suchen der Einträge einer Bikestation in einem zeitlichen Rahmen enorm unperformant.
Beide Probleme konnten durch spezifizierte Datenbankindexe gelöst werden. Jeder Dashboard-Endpoint hat seinen eigenen Index.
Beide Probleme konnten durch spezifische Datenbankindexe gelöst werden. Jeder Dashboard-Endpoint hat seinen eigenen Index.
Die Indexe sehen wie folgt aus:
```sql
CREATE INDEX IF NOT EXISTS "idx_date_of_start_date" ON usage_stats (date(start_date, 'unixepoch'))
CREATE INDEX IF NOT EXISTS "idx_end_station_id_date_of_start_date" ON "usage_stats" ("end_station_id" ASC, date(start_date, 'unixepoch'))
CREATE INDEX IF NOT EXISTS "idx_date_of_start_date" ON "usage_stats" (date("start_date", 'unixepoch'))
CREATE INDEX IF NOT EXISTS "idx_end_station_id_date_of_start_date" ON "usage_stats" ("end_station_id" ASC, date("start_date", 'unixepoch'))
CREATE INDEX IF NOT EXISTS "idx_start_station_id_date_of_start_date" ON "usage_stats" ("start_station_id" ASC, date("start_date", 'unixepoch'))
```
Jeder Index berechnet das Datum vor uns speichert es sozusagen als virtuelle Spalte in der Datenbank ab. Die zwei unteren betrachten hierzu noch die start- bzw. endstation.
Jeder Index berechnet das Datum vor und speichert es sozusagen als virtuelle Spalte in der Datenbank ab. Die zwei unteren Indexe betrachten hierzu noch die Start- bzw. Endstation.
Dies hat die Performance enorm verbessert.
@ -195,7 +195,7 @@ GROUP BY b.id_num
Die Berechnung dieser Tabelle dauert je nach Computer und Festplatte zwischen drei und zehn Minuten. Die Abfragedauer wird hierdurch um über 99 % reduziert, von etwa 20 Sekunden auf 10ms.
Durch all diese Optimierungen wuchs die Datenbank von 6.2 Gigabyte auf fast 10 Gigabyte an! Ein einzelner Index hat hierbei über 1.2 Gigabyte. Die initiale Ladezeit des Dashbaord eines belibigen Bikepoints wurde hierdurch von etwa 35 Sekunden auf unter eine Sekunde reduziert!
Durch all diese Optimierungen wuchs die Datenbank von 6.2 Gigabyte auf fast 10 Gigabyte an! Ein einzelner Index hat hierbei über 1.2 Gigabyte. Die initiale Ladezeit eines Dashboards wurde hierdurch von etwa 35 Sekunden auf unter eine Sekunde reduziert!
## Dockerization
Um das Backend einfach deployen zu können, haben wir uns für einen Docker Container entschieden. Dadurch ist der Server sehr leicht aufzusetzen, da nur Docker benötigt wird. Weiter bietet Docker die Möglichkeit einen abgestürzten Container automatisch neu zu starten. Es können zudem die neusten Features von Python 3.9 benutzt werden, da diese Version noch nicht in den APT-Repositories von Ubuntu 20.04 existiert.
@ -207,10 +207,10 @@ Aktuell wird nur der Source Code in das Image kopiert und die vorher generierte
# Frontend
## Leaflet im Detail
### Marker Generierung
Auch die Marker werden in unserer Applikation über den Map Service geladen. Wie bereits im [Kapitel 2 Vorgehensweise](Projektarbeit-3/Vorgehensweise) angedeutet wird die Map mit mehreren Methoden gefüllt. Jedes Layer oder Overlay besitzt eine eigene Methode.
Auch die Marker werden in unserer Applikation über den Map Service geladen. Wie bereits im [Kapitel 2 Vorgehensweise](Projektarbeit-3/Vorgehensweise) angedeutet wird die Map mit mehreren Methoden gefüllt. Jeder Layer und jedes Overlay besitzt eine eigene Methode.
Hier noch einmal die Methoden im Überblick:
```typscript
```typescript
this.service.initMap(51.509865, -0.118092, 14);
await this.service.drawStationMarkers();
this.service.drawHeatmap();
@ -219,17 +219,17 @@ this.service.drawAccidents();
Schauen wir uns im Weiteren die Methoden genauer an:
#### initMap
```typscript
```typescript
this.map = L.map('map').setView([lat, lon], zoom);
```
Wie bereits in der Vorgehensweise erläutert wird zuerst das Map-Objekt mit `L.map('map')` initialisiert.
Wie bereits in der Vorgehensweise erläutert, wird zuerst das Map-Objekt mit `L.map('map')` initialisiert.
```typescript
this.map.addLayer(new L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
...
...
```
Anschließend werden die Daten von OpenStreetMap als Base Layer gesetzt. So haben wir sichergestellt, dass die Map immer einen legitimen Hintergrund besitzt.
Anschließend werden die Daten von OpenStreetMap als Base Layer gesetzt. So haben wir sichergestellt, dass die Map immer einen aussagekräftigen Hintergrund besitzt.
```typescript
this.accidentLegend.onAdd = () => {
@ -239,13 +239,13 @@ this.accidentLegend.onAdd = () => {
this.map.on('overlayadd', e => e.name === 'Accidents' ? this.accidentLegend.addTo(this.map) : null);
this.map.on('overlayremove', e => e.name === 'Accidents' ? this.accidentLegend.remove() : null);
```
In dieser Methode wird unter anderem vorbereitend auf die Accidents die zugehörige Legende definiert. Innerhalb des `this.accidentLegend.onAdd {}` Blockes wird das DOM-Element erstellt, dass wenn die Accidents der Map aufgeschaltet werden, automatisch angezeigt und wieder entfernt wird.
In dieser Methode wird unter anderem vorbereitend auf die Accidents die zugehörige Legende definiert. Innerhalb des `this.accidentLegend.onAdd {}` Blockes wird das DOM-Element erstellt, dass, wenn die Accidents der Map aufgeschaltet werden, automatisch angezeigt und wieder entfernt wird.
#### drawStationMarkers
Diese Methode macht einen Aufruf an unser Backend und bezieht für alle 840 Stationen die nötigen Informationen, um die Anforderungen an das Pop-up als auch an die Routing Parameter zu erfüllen.
##### Clustering der Marker
Da 840 Marker auf der Map keinen ordentlichen Eindruck machen, haben wir mithilfe von `Leaflet.MarkerCluster` die Marker je nach Zoom-Level des Benutzers gruppiert.
Da 840 Marker auf der Map keinen ordentlichen Eindruck machen, haben wir mithilfe von `Leaflet.MarkerCluster` die Marker, je nach Zoom-Level des Benutzers, gruppiert.
```typescript
const markerClusters = L.markerClusterGroup({
spiderflyOnMaxZoom: true,
@ -259,11 +259,11 @@ Der MarkerCluster-Gruppe können bestimmte Optionen mitgegeben werden, wie z.B.:
<table>
<tr>
<td>spiderfyOnMaxZoom</td>
<td>Bei Klick auf ein Cluster, wird dieses automatisch aufgelöst und die Map zeigt die Marker</td>
<td>Bei Klick auf ein Cluster, wird dieser automatisch aufgelöst und die Map zeigt die darin enthaltenen Marker</td>
</tr>
<tr>
<td>showCoverageOnHover</td>
<td>Wenn man mit der Maus über ein Cluster hovert, zeigt dieses die Grenzen der Marker</td>
<td>Wenn man mit der Maus über ein Cluster hovert, zeigt dieser die Grenzen des Clusters</td>
</tr>
<tr>
<td>zoomToBoundsOnClick</td>
@ -279,34 +279,32 @@ Die letzten beiden Zeilen fügen zum einen die Marker dem Layer Control hinzu. U
##### Generierung der Marker
Anschließend iterieren wir durch die API-Response, welche die 840 Bikestationen hält.
```typscript
```typescript
for (const station of data) {
const marker = L.marker([station.lat, station.lon], {icon: createIcon('blue')});
markerClusters.addLayer(marker);
marker.on('click', e => {
e.target.bindPopup(this.popUpService.makeAvailabilityPopUp(station), {maxWidth: 'auto'})
.openPopup();
e.target.bindPopup(this.popUpService.makeAvailabilityPopUp(station), {maxWidth: 'auto'}).openPopup();
this.map.panTo(e.target.getLatLng());
});
marker.on('popupclose', e => e.target.unbindPopup());
}
}
```
Die Marker werden nicht direkt der Map hinzugefügt. Wie gerade ausgeführt gruppieren wir diese, das heißt wir fügen die Marker über das `markerClusters` Layer der Map hinzu.
Die Marker werden nicht direkt der Map hinzugefügt. Wie gerade ausgeführt gruppieren wir diese, das heißt wir fügen die Marker über dan `markerClusters` Layer der Map hinzu.
Anschließend werden dem Marker noch zwei Eventlistener hinzugefügt.
Anschließend werden dem Marker noch zwei Eventlistener gebunden.
```typescript
marker.on('click', e => {
e.target.bindPopup(this.popUpService.makeAvailabilityPopUp(station), {maxWidth: 'auto'})
.openPopup();
marker.on('click', e => {
e.target.bindPopup(this.popUpService.makeAvailabilityPopUp(station), {maxWidth: 'auto'}).openPopup();
this.map.panTo(e.target.getLatLng());
});
```
Dadurch wird erst bei Klick auf einen Marker, die entsprechende Komponente für das Pop-up generiert. Die Methoden des Pop-ups werden zu einem späteren Zeitpunkt genauer erläutert.
#### drawHeatmap
Bei der Heatmap haben wir uns entschieden das Plugin von Vladimir Agafonkin [Leaflet.heat](https://github.com/Leaflet/Leaflet.heat) zu nutzen. Das Plugin hat uns zugesagt, da die Heatpunkte die für die Generierung der Heatmap nötig sind, auch über ein Layer der Map hinzugefügt werden. Dies konnten nahtlos in unsere bestehende Architektur einbinden.
Bei der Heatmap haben wir uns entschieden, das Plugin von Vladimir Agafonkin [Leaflet.heat](https://github.com/Leaflet/Leaflet.heat) zu nutzen. Das Plugin hat uns zugesagt, da die Heatpunkte die für die Generierung der Heatmap nötig sind, auch über ein Layer der Map hinzugefügt werden. Dies konnte nahtlos in unsere bestehende Architektur einbinden.
In diesem Punkt hat sich aber das Datenmodell des Plugins leicht von unserem unterschieden und mussten daher unsere Bikepoints erst einmal mappen.
In diesem Punkt hat sich aber das Datenmodell des Plugins leicht von unserem unterschieden und musste daher an unsere Struktur angepasst werden.
<table>
<tr>
@ -337,7 +335,7 @@ export interface IMapBikePoint {
</tr>
</table>
Daher mussten wir bevor unsere Heatpoints dem Layer hinzufügen können, unsere Bikepoints reduzieren.
Daher mussten wir, bevor unsere Heatpoints dem Layer hinzufügen konnten, unsere Bikepoints reduzieren.
```typescript
const heatPoints = this.bikePoints.map(bikePoint => ([
@ -354,7 +352,7 @@ const heatmap = L.heatLayer(heatPoints, {
Enorm wichtig war die Wahl der Intensität. Diese beträgt in unserem Fall die Anzahl an Fahrrädern, die sich in der Station befinden. Außerdem unterscheidet sich die Heatmap gravierend je nach Radius des Heatpoints. Nach ausführlicher Analyse haben wir uns für einen Radius von 90 entschieden, um das optimale Ergebnis zu erhalten.
#### drawAccidents
Besonders an der Unfallstatistik ist das eigene Datenmodell. Da wir aber außerhalb dieser Methode nie mit einem Unfall hantieren, haben wir hierfür im Frontend kein eigenes Domänenobjekt erstellt.
Besonders an der Unfallstatistik ist das eigene Datenmodell. Da wir aber außerhalb dieser Methode nie mit einem Unfall arbeiten, haben wir hierfür im Frontend kein eigenes Domänenobjekt erstellt.
```typescript
const accidents = [];
@ -374,24 +372,23 @@ const accidentLayer = L.layerGroup(accidents);
```
Wir iterieren durch das API-Response und setzen als Erstes mit der Hilfsmethode `getAccidentColor` die Farbe des Markers. Diese unterscheidet lediglich nach Schwere des Unfalls zwischen Gelb, Orange und Rot.
Anschließend erstellen wir für jeden Unfall ein Objekt vom Typ Circle. Auch die Unfälle fügen wir der Map nicht direkt hinzu, sondern gehen über die `L.layerGroup`. So können wir auch die Unfälle in unserem Menü nach Belieben auf und abschalten.
Anschließend erstellen wir für jeden Unfall ein Objekt vom Typ `Circle`. Auch die Unfälle fügen wir der Map nicht direkt hinzu, sondern gehen über die `L.layerGroup`. So können wir auch die Unfälle in unserem Menü nach Belieben an- und abwählen.
## Erläuterung "the Angular way"
### Pop-up Generierung bei Marker-"click"
Eine besondere Komplexität und Schwierigkeit betraf die Generierung der Pop-up Komponente bei Klick auf einen Marker. Schnell hat sich herausgestellt, dass nicht ohne Weiteres die Methode `marker.bindPopup()` genutzt werden konnte. Da wir den Inhalt des Pop-up in eine eigene Komponente ausgelagert haben wird, sobald das Marker-Objekt initialisiert ist die OnInit-Methode der Komponente ausgeführt. Da dies aber erst geschehen darf, wenn der Benutzer auf einen Marker klickt, mussten wir hier einen anderen Weg finden.
Eine besondere Komplexität und Schwierigkeit betraf die Generierung der Pop-up Komponente bei Klick auf einen Marker. Schnell hat sich herausgestellt, dass nicht ohne Weiteres die Methode `marker.bindPopup()` genutzt werden konnte. Da wir den Inhalt des Pop-up in eine eigene Komponente ausgelagert haben, wird, sobald das Marker-Objekt initialisiert ist, die OnInit-Methode der Komponente ausgeführt. Da dies aber erst geschehen darf, wenn der Benutzer auf einen Marker klickt, mussten wir hier einen anderen Weg finden.
Nach ausführlicher Analyse der Leaflet-Dokumentation in Bezug auf Event-handling, haben wir uns für folgende Listener-Methode entschieden:
```typescript
object.on('event', methodToExecute);
```
Jedes Leaflet-Objekt hat sein eigenes Set an Events. Das erste Argument der Methode ist das Event-Objekt. Das zweite Argument ist die Methode, die bei Auftreten des Events ausgeführt werden soll.
Jedes Leaflet-Objekt hat sein eigenes Set an Events. Das erste Argument der Methode ist das Event-Objekt. Das zweite Argument ist die Funktion, die bei Auftreten des Events ausgeführt werden soll.
```typescript
marker.on('click', e => {
e.target.bindPopup(this.popUpService.makeAvailabilityPopUp(station), {maxWidth: 'auto'})
.openPopup();
e.target.bindPopup(this.popUpService.makeAvailabilityPopUp(station), {maxWidth: 'auto'}).openPopup();
this.map.panTo(e.target.getLatLng());
});
```
@ -412,22 +409,22 @@ makeAvailabilityPopUp(station: IMapBikePoint): any {
}
```
Im PopupService muss sichergestellt werden, dass eine Komponente programmatisch erstellt wird. Dies ist in Angular nur mittels des `ComponentFactoryResolver` möglich. Weiterhin bezieht das Availability-Chart seine Daten aus dem MapBikePoint. Diese wird auch über den ComponentFactoryResolver direkt gesetzt. Zum Schluss geben wir die Position des Pop-ups zurück.
Im `PopupService` muss sichergestellt werden, dass eine Komponente programmatisch erstellt wird. Dies ist in Angular nur mittels des `ComponentFactoryResolver` möglich. Weiterhin bezieht das Availability-Chart seine Daten aus dem `MapBikePoint`. Diese wird auch über den `ComponentFactoryResolver` direkt gesetzt. Zum Schluss geben wir die Position des Pop-ups zurück.
## Komponenten
### Map
Wie bereits in Kapitel 2 Vorgehensweise erläutert ist die MapComponent nur die grafische Darstellung der Map verantwortlich. Logik, betreffend auf das Generieren von Markern und weiteren Overlays ist in den zugehörigen MapService ausgelagert.
Wie bereits in Kapitel 2 Vorgehensweise erläutert ist die `MapComponent` nur die grafische Darstellung der Map verantwortlich. Logik, betreffend auf das Generieren von Markern und weiteren Overlays, ist in den zugehörigen `MapService` ausgelagert.
<div align="center">
<img src="uploads/2fae86faa6cbc4af7ae0bb3fd27d96ac/image.png"/>
</div>
Wie in der Grafik angedeutet, gibt die MapComponent dem Service lediglich Map-Optionen, wie z.B. Lat und Lon Koordinaten für die Zentrierung oder den initialen Zoom. Der MapService wiederum generiert das Pop-up und gibt der Komponente alle nötigen Layer.
Wie in der Grafik angedeutet, gibt die `MapComponent` dem Service lediglich Map-Optionen, wie z.B. Lat und Lon Koordinaten für die Zentrierung oder den initialen Zoom. Der `MapService` wiederum generiert das Pop-up und gibt der Komponente alle nötigen Layer.
#### Pop-up
Der Inhalt der PopUpComponent ist durch eine [Material Card](https://material.angular.io/components/card/examples) abgebildet. Als Titel ist der Name der Station zu sehen und als Body haben wir ein Bar Chart gewählt, welches anschaulich die noch verfügbaren Fahrräder in dieser Station visualisieren soll. Alle Charts wurden mit [Apex Charts](https://apexcharts.com/angular-chart-demos/) implementiert.
Der Inhalt der `PopUpComponent` ist durch eine [Material Card](https://material.angular.io/components/card/examples) abgebildet. Als Titel ist der Name der Station zu sehen und als Body haben wir ein Bar Chart gewählt, welches anschaulich die noch verfügbaren Fahrräder in dieser Station visualisieren soll. Alle Charts wurden mit [Apex Charts](https://apexcharts.com/angular-chart-demos/) implementiert.
Um nun ein Chart in einer Angular Komponente zu verwenden werden sogenannte ChartOptions benötigt. Das sind Optionen, die später in den HTML-Direktiven genutzt werden, um das Chart nach unseren Wünschen zu modellieren. In der `ngOnInit()` werden dann die ChartOptions gefüllt.
Um nun ein Chart in einer Angular Komponente zu verwenden, werden sogenannte ChartOptions benötigt. Das sind Optionen, die später in den HTML-Direktiven genutzt werden, um das Chart nach unseren Wünschen zu modellieren. In der `ngOnInit()` werden dann die ChartOptions gefüllt.
Wir gehen in der Dokumentation nur auf die Series ein. Also auf die Datensätze, die das Chart füllen. Andere Optionen sind relativ selbsterklärend. Das Chart für die Visualisierung der Verfügbarkeit von Fahrrädern benötigt folgende Datensätze:
@ -460,7 +457,7 @@ const NbBlockedDocks = this.station.status.NbDocks - this.station.status.NbBikes
### Toolbar
Die Toolbar ist auf Top-Level Ebene angesiedelt und wird sowohl im Dashboard als auch auf der Startseite angezeigt. Nur der Inhalt der Toolbar unterscheidet sich je nach Route:
<div> align="center">
<div align="center">
<table>
<tr>
<td>Startseite</td>
@ -521,16 +518,16 @@ Ist der Auto-Refresh aktiv, wird alle 10 Sekunden eine Methode im Map Service au
Das Dashboard selbst ist wieder in einzelne Unterkomponenten gegliedert. Dies sieht folgendermaßen aus:
<div align="center">
<img src="uploads/51075a24653d12ecba55222b2553ab53/image.png"/>
<img src="uploads/51075a24653d12ecba55222b2553ab53/image.png" width="60%" />
</div>
Das Dashboard selbst dient zum einen als Container für die ganzen Visualisierungen, als auch zum Delegieren des User-Inputs.
<div align="center">
<img src="uploads/ee944ca6c9f2956922421b14f487cfc0/image.png"/>
<img src="uploads/ee944ca6c9f2956922421b14f487cfc0/image.png" width="60%" />
</div>
Besonderheit unserer Codierung ist, dass wir alle Visualisierungen aufbauend auf den gewählten Start- & End-Datum implementiert haben. Wird also eine neue Zeitspanne vom User gewählt, so wird den einzelnen Komponenten die Daten weitergereicht und diese machen dann die notwendigen Aufrufe auf das Backend.
Besonderheit unserer Herangehensweise ist, dass wir alle Visualisierungen aufbauend auf den gewählten Start- & Enddatum implementiert haben. Wird also eine neue Zeitspanne vom User gewählt, so wird den einzelnen Komponenten die Daten weitergereicht und diese machen dann die notwendigen Aufrufe auf das Backend.
```typescript
async onSubmit(startEndDate: StartEndDate): Promise<any> {
@ -550,7 +547,7 @@ Besonderheit unserer Codierung ist, dass wir alle Visualisierungen aufbauend auf
```
### Table
Die zwei Tabellen haben bei der Implementierung die größte Aufmerksamkeit genossen. Da wir die Interaktion mit der Mini-Map als Must-have angesehen haben, ist hier auch sehr viel Fleiß in den Aufbau und die Generierung der Marker auf der Mini-Map.
Die zwei Tabellen haben bei der Implementierung die größte Aufmerksamkeit genossen. Da wir die Interaktion mit der Mini-Map als Must-have angesehen haben, ist hier auch sehr viel Fleiß in den Aufbau und die Generierung der Marker auf der Mini-Map geflossen.
Folgender schematischer Ablauf liegt der TableComponent zugrunde:
@ -580,10 +577,10 @@ selectRow(selection: MatCheckboxChange, row): void {
this.map.drawTableStationMarker(markerToDisplay);
}
```
Es wird, egal in welcher Tabelle eine Checkbox gedrückt wird, überprüft ob in der anderen Tabelle auch eine Station mit gleicher ID vorhanden ist. Ist dies der Fall, wird diese auch ausgewählt. Aus dem `SelectionModel` werden alle ausgewählten Stationen gefiltert und an die Map für die Marker Generierung weitergereicht. Davor wird allerdings noch mit `changePolyLineColorForDuplicateBikePoints` geprüft, ob Duplikate in der Liste vorhanden sind. Ist dem so, so wird die Polyline Farbe auf Blau gesetzt um Station, die in beiden Tabellen vorkommen noch einmal extra zu visualisieren.
Es wird, egal in welcher Tabelle eine Checkbox gedrückt wird, überprüft, ob in der anderen Tabelle dieselbe Station vorhanden ist. Ist dies der Fall, wird diese auch ausgewählt. Aus dem `SelectionModel` werden alle ausgewählten Stationen gefiltert und an die Map für die Marker Generierung weitergereicht. Davor wird allerdings noch mit `changePolyLineColorForDuplicateBikePoints` geprüft, ob Duplikate in der Liste vorhanden sind. Ist dem so, so wird die Polyline Farbe auf Blau gesetzt um Stationen, die in beiden Tabellen vorkommen noch einmal extra zu visualisieren.
### Rent Duration
In dieser Komponente wird das Chart für die durchschnittliche Ausleihdauer angezeigt. Dabei ist es wichtig, die richtigen Optionen von ApexCharts zu verwenden. Wenn das Dashboard geladen wird, wird jede Visualisierung mit den letztmöglichen Datum geladen. Beim ersten Aufruf des Charts müssen die Chartoptionen vollends angegeben werden. Wird dann vom User eine neue Zeitspanne angegeben, wird lediglich der Datensatz aktualisiert.
In dieser Komponente wird das Chart für die durchschnittliche Ausleihdauer angezeigt. Dabei ist es wichtig, die richtigen Optionen von ApexCharts zu verwenden. Wenn das Dashboard geladen wird, wird jede Visualisierung mit dem letztmöglichen Datum geladen. Beim ersten Aufruf des Charts müssen die Chartoptionen komplett angegeben werden. Wird dann vom User eine neue Zeitspanne angegeben, wird lediglich der Datensatz aktualisiert.
Vom Backend erhalten wir folgende Daten:
@ -645,7 +642,7 @@ this.chartOptions = {
</tr>
</table>
Gibt der User jetzt eine neue Zeitspanne an, wird mit folgender Methode aus dem Dashboard heraus aufgerufen:
Gibt der User jetzt eine neue Zeitspanne an, wird folgende Methode aus dem Dashboard heraus aufgerufen:
```typescript
async onSubmit(actualStartDate: string, actualEndDate: string): Promise<void> {
@ -737,7 +734,7 @@ location / {
try_files $uri $uri/ /index.html;
}
```
Die erste Direktive nimmt alle URLs mit "api" und leitet sich an unser Backend weiter. Die zweite leitet alle anderen Requests auf die index.html Seite weiter, da Angular als Single-Page-App alle Routen über die gleiche Datei verarbeitet.
Die erste Direktive nimmt alle URLs mit "api" und leitet sich an unser Backend weiter. Die zweite leitet alle anderen Requests auf die index.html Seite weiter, da Angular als Single-Page-Applikation alle Routen über die gleiche Datei verarbeitet.
**Anmerkung:** Es ist nicht schlimm, dass auf das Backend intern ohne HTTPS zugegriffen wird, da Nginx allen anderen SSL Traffic nach außen abdeckt, und auf der Loopback-Adresse des Servers keine Gefahr besteht.