From a648a49a7c1ad2580c6df7f3855aeac3fa98d8be Mon Sep 17 00:00:00 2001 From: Konstantinos Kamaropoulos Date: Tue, 3 Mar 2020 03:04:44 +0200 Subject: [PATCH] docs: Add comments to most important files --- src/app/chart/chart.component.ts | 68 ++++++++++++++++++++++++++++- src/app/logs.service.ts | 37 ++++++++++------ src/app/map/map.component.html | 5 --- src/app/map/map.component.ts | 73 ++++++++++++++++++++++++++++---- 4 files changed, 156 insertions(+), 27 deletions(-) diff --git a/src/app/chart/chart.component.ts b/src/app/chart/chart.component.ts index 5890731..82d1d16 100644 --- a/src/app/chart/chart.component.ts +++ b/src/app/chart/chart.component.ts @@ -11,14 +11,21 @@ import { interval } from 'rxjs'; styleUrls: ['./chart.component.scss'] }) export class ChartComponent implements OnInit { + // The subscribe object for the rxjs interval sub: any; constructor(private logsService: LogsService) {} + // We're going to keep all our data here. + // This is a two dimmensional Object array. + // It keeps all the data points for every sensor we have to display on the chart. dataPoints: Array> = []; + // Global object to keep our chart. chart: any; + // Method to toggle data series visibility on and off. + // This will be used on the chart legend. toggleDataSeries(e: any) { if (typeof e.dataSeries.visible === 'undefined' || e.dataSeries.visible) { e.dataSeries.visible = false; @@ -28,14 +35,22 @@ export class ChartComponent implements OnInit { this.chart.render(); } + // Variable to store the data config of our chart. + // We'll fill this with stuff about all of our sensors later on. dataConfig: Array = []; async ngOnInit() { + // OnInit, get the logs for the first time let data = await this.logsService.getLogsFirstRun(); + + // For every sensor in the sensorReadings for (const key in data[0]['sensorReadings']) { if (data[0]['sensorReadings'].hasOwnProperty(key)) { + // Get the value for the sensor const element = data[0]['sensorReadings'][key]; + // Create a new array inside dataPoint for the sensor this.dataPoints[key] = []; + // Add configuration for the sensor this.dataConfig.push({ type: 'line', xValueType: 'dateTime', @@ -47,20 +62,31 @@ export class ChartComponent implements OnInit { }); } } + let dpsLength = 0; + + // Create a new CanvasJS chart on #chartContainer this.chart = new CanvasJS.Chart('chartContainer', { zoomEnabled: true, animationEnabled: true, exportEnabled: true, + // When we have large time gaps between our data, + // it'd be better to shrink it so it doesn't mess up + // the way our useful data is displayed. + // That's where scaleBreaks come in. axisX: { scaleBreaks: { autoCalculate: true, maxNumberOfAutoBreaks: 5 } }, + // Share the same tooltip between on data lines. toolTip: { shared: true }, + // Set up a legend. + // We also want to be able to toggle our data series, + // so we do that with toggleDataSeries on click. legend: { cursor: 'pointer', verticalAlign: 'top', @@ -68,46 +94,84 @@ export class ChartComponent implements OnInit { fontColor: 'dimGrey', itemclick: this.toggleDataSeries }, + // That's the sensor series configuration we created earlier. data: [...this.dataConfig] }); + // Add the createdAt field of the log to the sensorReadings object + // for ever log item we have. This way we will have it available + // without doing anything weird later on. let sensorReadings: any = data.map(log => { + // Dump the logs into a new object so we don't have to touch the original one let d = log['sensorReadings']; + + // and add the createdAt field to it. d['time'] = log['createdAt']; + + // Then we just return the whole thing. return d; }); - let i = 0; - + // For every log we have sensorReadings.forEach((element: any) => { for (const key in element) { + // and for every field/sensor that is not time, since we just need this for the X Axis if (element.hasOwnProperty(key) && key != 'time') { + // keep the value of the sensor const value = element[key]; + + // and add a new data point for that sensor, with it's createdAt time + // on the X Axis and the sensor value on the Y Axis. this.dataPoints[key].push({ x: new Date(element['time']), y: parseInt(value) }); + + // Set the lenght of the datapoints to the amount of datapoints after we added them dpsLength = this.dataPoints.length; } } }); + + // Now that we've added some data to the chart, render it. this.chart.render(); + // Run the chart update every second. this.sub = interval(1000).subscribe(async val => { + // If live updates are enabled: if (this.logsService.liveUpdate) { + // Get the changes since the last time we got the logs. let data = await this.logsService.getUpdates(); + + // Again, we need to add the createAt field of the log to the sensor readings object. let sensorReadings: any = data.map(log => { + // Dump the logs into a new object so we don't have to touch the original one let d = log['sensorReadings']; + + // and add the createdAt field to it. d['time'] = log['createdAt']; + + // Then we just return the whole thing. return d; }); + + // For every new log we got sensorReadings.forEach((element: any) => { for (const key in element) { + // and for every field/sensor that is not time, since we just need this for the X Axis if (element.hasOwnProperty(key) && key != 'time') { + // keep the value of the sensor const value = element[key]; + + // and add a new data point for that sensor, with it's createdAt time + // on the X Axis and the sensor value on the Y Axis. this.dataPoints[key].push({ x: new Date(element['time']), y: parseInt(value) }); + + // Set the lenght of the datapoints to the amount of datapoints after we added them dpsLength = this.dataPoints.length; } } dpsLength++; }); + + // Render the chart again, in case we've added any new log values to it. this.chart.render(); } }); diff --git a/src/app/logs.service.ts b/src/app/logs.service.ts index dd76eee..c515770 100644 --- a/src/app/logs.service.ts +++ b/src/app/logs.service.ts @@ -9,40 +9,53 @@ import { map, catchError, last } from 'rxjs/operators'; export class LogsService { constructor(private httpClient: HttpClient) {} + // Toggle whether to run live updates on the data or not liveUpdate: Boolean = true; + // Global variable for getting only the changes from latest logs previousData: Array | undefined = undefined; getLogs(): Promise> { - return this.httpClient - .get('/logs') - .pipe( - map((body: any) => { - return body; - }), - catchError(() => of('Error, could not load logs')) - ) - .toPromise(); + // Get the logs from the API + return ( + this.httpClient + .get('/logs') + .pipe( + map((body: any) => { + return body; + }), + catchError(() => of('Error, could not load logs')) + ) + // Convert Observable to Promise for easier manipulation and get some async/await convinience + .toPromise() + ); } + // Get the logs from the API on the first time we ever need them async getLogsFirstRun(): Promise> { let data = await this.getLogs(); this.previousData = data; return data; } + // Get only the logs we haven't seen before async getUpdates(): Promise> { - let prevData = this.previousData; + // If previousData is undefined, aka this is our first time here: if (!this.previousData) { + // Get the logs from the API let data = await this.getLogs(); + // and store them to previousData this.previousData = data; + // Lastly, return the data we got from the API return data; } else { + // It's not our first time here, get the logs let latestData = await this.getLogs(); - + // Get only the differences between latestData and previousData var changes = latestData.filter(item1 => !this.previousData.some(item2 => item2['_id'] === item1['_id'])); - + // Set previousData to the latest version of the logs we just got this.previousData = latestData; + // Return the changes return changes; } } diff --git a/src/app/map/map.component.html b/src/app/map/map.component.html index e47e9c4..ec96ca0 100644 --- a/src/app/map/map.component.html +++ b/src/app/map/map.component.html @@ -1,9 +1,4 @@
- - -
diff --git a/src/app/map/map.component.ts b/src/app/map/map.component.ts index d025639..46f3fa6 100644 --- a/src/app/map/map.component.ts +++ b/src/app/map/map.component.ts @@ -13,26 +13,40 @@ import { interval } from 'rxjs'; styleUrls: ['./map.component.scss'] }) export class MapComponent implements OnInit { - version: string | null = environment.version; - - pointsToDisplay = 10; - + // Global map object map: mapboxgl.Map; + + // Style of the map we'll be loading. + // Can also be 'mapbox://styles/mapbox/satellite-v9' for a satellite map style = 'mapbox://styles/mapbox/streets-v11'; + + // Coordinates for the location we're going to focus initially. + // In this case Athens, Greece. lat = 37.9838; lng = 23.7275; + // The subscribe object for the rxjs interval sub: any; + // The last point we've added to the map. + // It's going to be a two element array, one for longitude and one for latitude. lastPoint: number[]; - ommitedPoints: number = 0; + // The maximum number of points to display on the first load. + // This will be the last X points we get from the logs API. + pointsToDisplay = 10; + + // The number of displayed points in the map + // and how many we've ommited in order to increase performance displayedPoints: number = 0; + ommitedPoints: number = 0; constructor(private logsService: LogsService) {} async ngOnInit() { + // Load access token into the mapboxx object Object.getOwnPropertyDescriptor(mapboxgl, 'accessToken').set(environment.mapbox.accessToken); + // Create map on the #map div this.map = new mapboxgl.Map({ container: 'map', style: this.style, @@ -40,51 +54,94 @@ export class MapComponent implements OnInit { zoom: 9 }); - // Add map controls - // this.map.addControl(new mapboxgl.NavigationControl()); - + // Check to see if it's the first run of the component var firstRun: Boolean = true; + // Run every second this.sub = interval(1000).subscribe(async val => { + // If it's the first time we run this or if live updates are enabled (firstTime overides liveUpdate) if (this.logsService.liveUpdate || firstRun) { let data: Array; if (firstRun) { + // Get logs for the first time data = await this.logsService.getLogsFirstRun(); + // Since we got our data, set firstRun to false firstRun = false; + // If we get more data than the maximum allowed displayed points, + // ommit the excess data points and keep the count of how many points we ommited. if (data.length > this.pointsToDisplay) { this.ommitedPoints = data.length - this.pointsToDisplay; data = data.slice(data.length - this.pointsToDisplay, data.length); } } else { + // If this is not the first time we run this, get only the changes since the last time we got the logs data = await this.logsService.getUpdates(); } + + // Here we are now getting new points every second. + // Those will be added on top of the maximum loaded points + // so we should keep count of how many points we're adding so we can display + // the number of displayed points on the map. + + // Initialize new point counter. + // This will decide if we actually got any new points + // and whether we actually have to fly to the new marker in the map. let newPointsCount = 0; + // Add the number of data points we got to the number of displayed points this.displayedPoints = this.displayedPoints + data.length; + + // For every log we got for (let log of data) { + // We only add markers for logs with valid GPS data, aka only status == A. if (log['gps_data']['status'] == 'A') { + // We got a new valid point to add a marker for. + // This means we'll have to fly to it. + // Increase the relevant counter. newPointsCount++; + + // Some dirty templating for the data inside the popup box. + // This should normally be done in a new component but yeah... + + // Here we will display the sensor readings for the specific log, + // as well as it's date and time. + + // Those are going to be displayed withing an HTML table. + // Create the table and add the header. let html = log['gps_data']['datestamp'] + ' ' + log['gps_data']['timestamp'] + '
'; + // Add tr with name and value for every sensor for (let record in log['sensorReadings']) { if (Object.prototype.hasOwnProperty.call(log['sensorReadings'], record)) { html += '' + ''; } } + + // Finally, add the table closing tag. html += '
Sensor     Reading
' + record + '' + log['sensorReadings'][record] + '
'; + + // Create a new popup with the HTML code we created above var popup = new mapboxgl.Popup({ offset: 25 }).setHTML(html); + + // Create a new marker on the log location and attach the newly created popup to it var marker = new mapboxgl.Marker({ draggable: false }) .setLngLat([log['gps_data']['longitude'], log['gps_data']['latitude']]) .setPopup(popup) .addTo(this.map); + + // When we are adding multiple markers, we need a way to keep track of the last one, + // in order to be able to fly to it. That's what we'll be doing here. this.lastPoint = [log['gps_data']['longitude'], log['gps_data']['latitude']]; } } + + // If we got any new markers if (newPointsCount) { + // fly to the last marker we added. this.map.flyTo({ center: [this.lastPoint[0], this.lastPoint[1]], essential: true, // this animation is considered essential with respect to prefers-reduced-motion