Real-time communication with frontend

- Frontend shows heatmap of most visit places
- Maximum accuracy can now be set
- Fix bug where battery chart filtered values wrongly
This commit is contained in:
2020-10-26 23:38:34 +01:00
parent fa60f58d3c
commit e12ed7775b
20 changed files with 770 additions and 161 deletions

View File

@@ -1,12 +1,15 @@
import { HttpClient, HttpHeaders, HttpParams } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { MqttService } from 'ngx-mqtt';
import { BehaviorSubject } from 'rxjs';
import * as moment from 'moment';
export interface ILogin {
token: string;
}
export interface IBeat {
_id: string;
coordinate?: number[];
accuracy: number;
speed: number;
@@ -35,8 +38,7 @@ export enum UserType {
export interface IUser {
name: string;
password: string;
salt: string;
brokerToken: string;
type: UserType;
lastLogin: Date;
twoFASecret?: string;
@@ -67,8 +69,15 @@ export class APIService {
private token: string;
username: string;
time: ITimespan | undefined;
rabbitmq: any;
time: ITimespan | undefined = {
from: moment().subtract(1, 'day').unix(),
to: moment().unix()
};
// Passthough data (not useful for api but a way for components to share data)
showFilter = true;
maxAccuracy: BehaviorSubject<number> = new BehaviorSubject(30);
// Cached data
beats: IBeat[];
@@ -79,9 +88,8 @@ export class APIService {
phones: IPhone[];
user: IUser = {
name: '',
brokerToken: '',
lastLogin: new Date(2020, 3, 1),
password: '',
salt: '',
type: UserType.GUEST,
createdAt: new Date(),
twoFASecret: ''
@@ -95,7 +103,7 @@ export class APIService {
API_ENDPOINT = 'http://192.168.178.26:8040';
constructor(private httpClient: HttpClient) { }
constructor(private httpClient: HttpClient, private mqtt: MqttService) {}
async login(username: string, password: string): Promise<ILogin> {
return new Promise<ILogin>(async (resolve, reject) => {
@@ -107,6 +115,25 @@ export class APIService {
this.username = username;
await this.getPhones();
await this.getUserInfo();
// Connect with RabbitMQ after we received our user information
this.mqtt.connect({
hostname: '192.168.178.26',
port: 15675,
protocol: 'ws',
path: '/ws',
username: this.user.name,
password: this.user.brokerToken
});
this.mqtt.observe('/').subscribe(message => {
if (this.beats !== undefined) {
this.beats.push(JSON.parse(message.payload.toString()) as IBeat);
this.beatsEvent.next(this.beats);
this.beatStats.totalBeats++;
}
});
await this.getBeats();
await this.getBeatStats();
this.loginEvent.next(true);

View File

@@ -14,6 +14,7 @@ import { LoginComponent } from './login/login.component';
import { MapComponent } from './map/map.component';
import { UserComponent } from './user/user.component';
import { DashboardWidgetComponent } from './dashboard-widget/dashboard-widget.component';
import { IMqttServiceOptions, MqttModule } from 'ngx-mqtt';
@NgModule({
declarations: [
@@ -31,6 +32,7 @@ import { DashboardWidgetComponent } from './dashboard-widget/dashboard-widget.co
BrowserAnimationsModule,
FormsModule,
HttpClientModule,
MqttModule.forRoot({}),
NgxMapboxGLModule.withConfig({
accessToken: 'pk.eyJ1IjoibW9uZGVpMSIsImEiOiJja2dsY2ZtaG0xZ2o5MnR0ZWs0Mm82OTBpIn0.NzDWN3P6jJLmci_v3MM1tA'
}),

View File

@@ -1,6 +1,7 @@
import { AfterViewInit, Component, OnInit } from '@angular/core';
import { ChartDataSets, ChartOptions } from 'chart.js';
import { Label } from 'ng2-charts';
import * as moment from 'moment';
import { APIService, IBeat } from '../api.service';
@Component({
@@ -51,10 +52,12 @@ export class DashboardComponent implements AfterViewInit {
this.lineChartLabels = [];
const batteryLevels: number[] = [];
let currentLevel = 0;
const finalBeats = beats.filter((val, i, array) => {
if (batteryLevels.indexOf(val.battery) === -1) {
if (currentLevel !== val.battery) {
batteryLevels.push(val.battery);
currentLevel = val.battery;
return true;
} else {
return false;
@@ -63,7 +66,7 @@ export class DashboardComponent implements AfterViewInit {
finalBeats.forEach((beat) => {
this.lineChartData[0].data.push(beat.battery);
this.lineChartLabels.push(this.formatDateTime(new Date(beat.createdAt)));
this.lineChartLabels.push(moment(new Date(beat.createdAt)).format(this.lineChartOptions.scales.xAxes[0].time.parser.toString()));
});
let tDistance = 0;
@@ -83,10 +86,6 @@ export class DashboardComponent implements AfterViewInit {
});
}
private formatDateTime(date: Date): string {
return `${date.getMonth()}/${date.getDay()}/${date.getFullYear()} ${date.getHours()}:${date.getMinutes()}:${date.getSeconds()}`;
}
ngAfterViewInit(): void {
}

View File

@@ -1,5 +1,7 @@
<div id="filter" [ngClass]="{hide: !this.api.showFilter}" *ngIf="this.api.loginEvent.value">
<h3 (click)="update()" style="cursor: pointer;">Refresh</h3>
<!-- Time range -->
<select (change)="update($event.target.value)" [(ngModel)]="this.presetHours">
<option value="-1">Today</option>
<option value="3">Last 3h</option>
@@ -17,4 +19,8 @@
<option value="month">Months</option>
<option value="year">Years</option>
</select>
<!-- Max accuracy -->
<h4>Max accuracy</h4>
<input class="customRange" (change)="updateAccuracy($event)">
</div>

View File

@@ -40,6 +40,10 @@ export class FilterComponent implements OnInit {
this.refresh();
}
updateAccuracy(val: any): void {
this.api.maxAccuracy.next(Number(val.target.value));
}
async refresh(): Promise<void> {
await this.api.getBeats();
await this.api.getBeatStats();

View File

@@ -1,5 +1,6 @@
<mgl-map [style]="'mapbox://styles/mapbox/dark-v10'" [zoom]="[15]" [center]="[this.lastLocation[0], this.lastLocation[1]]" *ngIf="showMap">
<mgl-geojson-source id="locHistory" [data]="data"></mgl-geojson-source>
<mgl-geojson-source id="locHistoryFiltered" [data]="mostVisitData"></mgl-geojson-source>
<mgl-geojson-source id="lastLoc" [data]="lastLocationData"></mgl-geojson-source>
<mgl-layer
id="locHistory"
@@ -10,6 +11,12 @@
'line-width': 3
}"
></mgl-layer>
<mgl-layer
id="locHistoryHeatmap"
type="heatmap"
source="locHistoryFiltered"
[paint]="mostVisitPaint"
></mgl-layer>
<mgl-layer
id="lastLoc"
type="circle"

View File

@@ -1,5 +1,4 @@
import { AfterViewInit, Component, OnInit } from '@angular/core';
import { Map } from 'mapbox-gl';
import { APIService } from '../api.service';
@Component({
@@ -7,9 +6,8 @@ import { APIService } from '../api.service';
templateUrl: './map.component.html',
styleUrls: ['./map.component.scss']
})
export class MapComponent implements AfterViewInit {
export class MapComponent {
map: Map;
lastLocation: number[] = [0, 0];
showMap = false;
@@ -21,6 +19,79 @@ export class MapComponent implements AfterViewInit {
geometry: { type: 'LineString', coordinates: [] }
}]
};
mostVisitData: GeoJSON.FeatureCollection<GeoJSON.LineString> = {
type: 'FeatureCollection',
features: [{
type: 'Feature',
properties: null,
geometry: { type: 'LineString', coordinates: [] }
}]
};
mostVisitPaint = {
// Increase the heatmap weight based on frequency and property magnitude
'heatmap-weight': [
'interpolate',
['linear'],
['get', 'mag'],
0,
0,
6,
1
],
// Increase the heatmap color weight weight by zoom level
// heatmap-intensity is a multiplier on top of heatmap-weight
'heatmap-intensity': [
'interpolate',
['linear'],
['zoom'],
0,
3,
9,
1
],
// Color ramp for heatmap. Domain is 0 (low) to 1 (high).
// Begin color ramp at 0-stop with a 0-transparancy color
// to create a blur-like effect.
'heatmap-color': [
'interpolate',
['linear'],
['heatmap-density'],
0,
'rgba(33,102,172,0)',
0.3,
'rgb(103,169,207)',
0.4,
'rgb(209,229,240)',
0.7,
'rgb(253,219,199)',
0.95,
'rgb(239,138,98)',
1,
'rgb(178,24,43)'
],
// Adjust the heatmap radius by zoom level
'heatmap-radius': [
'interpolate',
['linear'],
['zoom'],
0,
2,
7,
10,
9,
15
],
// Transition from heatmap to circle layer by zoom level
'heatmap-opacity': [
'interpolate',
['linear'],
['zoom'],
16,
1,
17,
0
]
};
lastLocationData: GeoJSON.FeatureCollection<GeoJSON.Point> = {
type: 'FeatureCollection', features: [
@@ -54,33 +125,44 @@ export class MapComponent implements AfterViewInit {
this.api.beatsEvent.subscribe(beats => {
if (beats.length === 0) { return; }
this.lastLocationPaint['circle-radius'].stops[1][1] = this.metersToPixelsAtMaxZoom(
beats[0].accuracy, this.lastLocation[0]
);
this.update();
this.lastLocationPaint['circle-radius'].stops[1][1] = this.metersToPixelsAtMaxZoom(
beats[beats.length - 1].accuracy, this.lastLocation[0]
);
this.lastLocationPaint = { ...this.lastLocationPaint };
this.api.maxAccuracy.subscribe(val => {
this.buildMap(val);
});
});
}
/* Function to draw circle with exact size by
/* Function to draw circle with exact size from
https://stackoverflow.com/questions/37599561/drawing-a-circle-with-the-radius-in-miles-meters-with-mapbox-gl-js
*/
private metersToPixelsAtMaxZoom(meters: number, latitude: number): number {
return meters / 0.075 / Math.cos(latitude * Math.PI / 180);
}
/* Function to find out if a provided point is in the area from
https://stackoverflow.com/questions/24680247/check-if-a-latitude-and-longitude-is-within-a-circle-google-maps
*/
private isPointInRadius(checkPoint: { lat: number, lng: number }, centerPoint: { lat: number, lng: number }, km: number): boolean {
const ky = 40000 / 360;
const kx = Math.cos(Math.PI * centerPoint.lat / 180.0) * ky;
const dx = Math.abs(centerPoint.lng - checkPoint.lng) * kx;
const dy = Math.abs(centerPoint.lat - checkPoint.lat) * ky;
return Math.sqrt(dx * dx + dy * dy) <= km;
}
async update(): Promise<void> {
this.data.features[0].geometry.coordinates = [];
// Add lines to map backwards (because it looks cool)
for (let i = this.api.beats.length - 1; i >= 0; i--) {
const beat = this.api.beats[i];
this.data.features[0].geometry.coordinates.push([beat.coordinate[1], beat.coordinate[0]]);
this.data = { ... this.data };
}
console.log('Last', this.api.beats[0]);
this.buildMap();
this.lastLocation = [ this.api.beats[0].coordinate[1],
this.api.beats[0].coordinate[0] ];
this.lastLocation = [this.api.beats[this.api.beats.length - 1].coordinate[1],
this.api.beats[this.api.beats.length - 1].coordinate[0]];
this.lastLocationData.features[0].geometry.coordinates = this.lastLocation;
this.lastLocationData = { ...this.lastLocationData };
@@ -88,7 +170,46 @@ export class MapComponent implements AfterViewInit {
this.showMap = true;
}
async ngAfterViewInit(): Promise<void> {
buildMap(maxAccuracy: number = 30): void {
const mostVisit = new Map<string, number>();
this.data.features[0].geometry.coordinates = [];
this.mostVisitData.features[0].geometry.coordinates = [];
for (let i = 0; i < this.api.beats.length; i++) {
const beat = this.api.beats[i];
if (beat.accuracy > maxAccuracy) { continue; }
this.data.features[0].geometry.coordinates.push([beat.coordinate[1], beat.coordinate[0]]);
// Get most visit points
for (let b = 0; b < this.api.beats.length; b++) {
const beat2 = this.api.beats[b];
const isNearPoint = this.isPointInRadius(
{ lat: beat2.coordinate[0], lng: beat2.coordinate[1] },
{ lat: beat.coordinate[0], lng: beat.coordinate[1] },
0.02
);
if (isNearPoint) {
if (mostVisit.has(beat2._id)) {
mostVisit.set(beat2._id, mostVisit.get(beat2._id) + 1);
} else {
mostVisit.set(beat2._id, 1);
}
}
}
}
for (const [key, value] of mostVisit) {
if (value < 2) { continue; }
this.api.beats.forEach(beat => {
if (beat._id === key) {
this.mostVisitData.features[0].geometry.coordinates.push([beat.coordinate[1], beat.coordinate[0]]);
}
});
}
this.data = { ... this.data };
this.mostVisitData = { ... this.mostVisitData };
}
}

View File

@@ -1,6 +1,7 @@
@import '../../styles.scss';
#user {
min-width: 40rem;
margin-top: 3rem;
margin-left: 20rem;
margin-right: 20rem;