Notification system in frontend

- Heatmap worker now reports progress
- Base for new altitude value
- Phones can be marked active
This commit is contained in:
2020-11-13 19:04:45 +01:00
parent d747b7be5b
commit ab1b90d020
27 changed files with 423 additions and 36 deletions

View File

@@ -92,7 +92,10 @@ class TrackerService : Service() {
level * 100 / scale.toFloat() level * 100 / scale.toFloat()
} }
val beat = Beat(androidId, arrayOf(location.latitude, location.longitude, location.accuracy.toDouble(), location.speed.toDouble()), batteryPct?.toInt(), location.time) val beat = Beat(
androidId,
arrayOf(location.latitude, location.longitude, location.altitude, location.accuracy.toDouble(), location.speed.toDouble()),
batteryPct?.toInt(), location.time)
val moshi = Moshi.Builder().add(KotlinJsonAdapterFactory()).build() val moshi = Moshi.Builder().add(KotlinJsonAdapterFactory()).build()
val jsonAdapter: JsonAdapter<Beat> = moshi.adapter(Beat::class.java) val jsonAdapter: JsonAdapter<Beat> = moshi.adapter(Beat::class.java)

View File

@@ -7,7 +7,7 @@ import { Phone } from "../models/phone/phone.model";
export async function GetPhone(req: LivebeatRequest, res: Response) { export async function GetPhone(req: LivebeatRequest, res: Response) {
const phoneId: String = req.params['id']; const phoneId: String = req.params['id'];
// If none id provided, return all. // If no id provided, return all.
if (phoneId === undefined) { if (phoneId === undefined) {
const phone = await Phone.find({ user: req.user?._id }); const phone = await Phone.find({ user: req.user?._id });
res.status(200).send(phone); res.status(200).send(phone);
@@ -48,8 +48,7 @@ export async function PostPhone(req: LivebeatRequest, res: Response) {
const phone = await Phone.findOne({ androidId, user: req.user?._id }); const phone = await Phone.findOne({ androidId, user: req.user?._id });
if (phone !== null) { if (phone !== null) {
logger.debug("Request to /phone failed because phone already exists."); res.status(409).send({ message: "This phone already exists." });
res.status(409).send();
return; return;
} }
@@ -60,7 +59,8 @@ export async function PostPhone(req: LivebeatRequest, res: Response) {
modelName, modelName,
operatingSystem, operatingSystem,
architecture, architecture,
user: req.user?._id user: req.user?._id,
active: false
}); });
logger.info(`New device (${displayName}) registered for ${req.user?.name}.`) logger.info(`New device (${displayName}) registered for ${req.user?.name}.`)

View File

@@ -30,13 +30,14 @@ export class RabbitMQ {
return; return;
} }
logger.info(`New beat from ${phone.displayName} with ${msg.gpsLocation[2]} accuracy and ${msg.battery}% battery`); logger.info(`New beat from ${phone.displayName} with ${msg.gpsLocation[2]}, ${msg.gpsLocation[3]}m height and accuracy and ${msg.battery}% battery`);
const newBeat = await Beat.create({ const newBeat = await Beat.create({
phone: phone._id, phone: phone._id,
coordinate: [msg.gpsLocation[0], msg.gpsLocation[1]], // [latitude, longitude, altitude]
accuracy: msg.gpsLocation[2], coordinate: [msg.gpsLocation[0], msg.gpsLocation[1], msg.gpsLocation[2]],
speed: msg.gpsLocation[3], accuracy: msg.gpsLocation[3],
speed: msg.gpsLocation[4],
battery: msg.battery, battery: msg.battery,
createdAt: msg.timestamp createdAt: msg.timestamp
}); });

View File

@@ -2,6 +2,7 @@ import { Document } from 'mongoose';
import { IPhone } from '../phone/phone.interface'; import { IPhone } from '../phone/phone.interface';
export interface IBeat extends Document { export interface IBeat extends Document {
// [latitude, longitude, altitude, accuracy, speed]
coordinate?: number[], coordinate?: number[],
accuracy: number, accuracy: number,
speed: number, speed: number,

View File

@@ -8,6 +8,7 @@ export interface IPhone extends Document {
operatingSystem: String, operatingSystem: String,
architecture: String, architecture: String,
user: IUser, user: IUser,
active: Boolean,
updatedAt?: Date, updatedAt?: Date,
createdAt?: Date createdAt?: Date
} }

View File

@@ -7,7 +7,8 @@ const schemaPhone = new Schema({
modelName: { type: String, required: false }, modelName: { type: String, required: false },
operatingSystem: { type: String, required: false }, operatingSystem: { type: String, required: false },
architecture: { type: String, required: false }, architecture: { type: String, required: false },
user: { type: SchemaTypes.ObjectId, required: true } user: { type: SchemaTypes.ObjectId, required: true },
active: { type: Boolean, required: true }
}, { }, {
timestamps: { timestamps: {
createdAt: true, createdAt: true,

View File

@@ -31,7 +31,8 @@
"styles": [ "styles": [
"src/styles.scss", "src/styles.scss",
"./node_modules/mapbox-gl/dist/mapbox-gl.css", "./node_modules/mapbox-gl/dist/mapbox-gl.css",
"./node_modules/@mapbox/mapbox-gl-geocoder/lib/mapbox-gl-geocoder.css" "./node_modules/@mapbox/mapbox-gl-geocoder/lib/mapbox-gl-geocoder.css",
"../node_modules/font-awesome/css/font-awesome.css"
], ],
"scripts": [ "scripts": [
"./node_modules/chart.js/dist/Chart.min.js" "./node_modules/chart.js/dist/Chart.min.js"

View File

@@ -1653,6 +1653,51 @@
"to-fast-properties": "^2.0.0" "to-fast-properties": "^2.0.0"
} }
}, },
"@fortawesome/angular-fontawesome": {
"version": "0.7.0",
"resolved": "https://registry.npmjs.org/@fortawesome/angular-fontawesome/-/angular-fontawesome-0.7.0.tgz",
"integrity": "sha512-U+eHYbKuVYrrm9SnIfl+z+6KTiI4Pu+S2OKh34JIi7C1jHhDcrVeDZISP/cpswHY7LWWDOPYeKE+yuWFlL4aVw==",
"requires": {
"tslib": "^2.0.0"
}
},
"@fortawesome/fontawesome-common-types": {
"version": "0.2.32",
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-0.2.32.tgz",
"integrity": "sha512-ux2EDjKMpcdHBVLi/eWZynnPxs0BtFVXJkgHIxXRl+9ZFaHPvYamAfCzeeQFqHRjuJtX90wVnMRaMQAAlctz3w=="
},
"@fortawesome/fontawesome-svg-core": {
"version": "1.2.32",
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-1.2.32.tgz",
"integrity": "sha512-XjqyeLCsR/c/usUpdWcOdVtWFVjPbDFBTQkn2fQRrWhhUoxriQohO2RWDxLyUM8XpD+Zzg5xwJ8gqTYGDLeGaQ==",
"requires": {
"@fortawesome/fontawesome-common-types": "^0.2.32"
}
},
"@fortawesome/free-brands-svg-icons": {
"version": "5.15.1",
"resolved": "https://registry.npmjs.org/@fortawesome/free-brands-svg-icons/-/free-brands-svg-icons-5.15.1.tgz",
"integrity": "sha512-pkTZIWn7iuliCCgV+huDfZmZb2UjslalXGDA2PcqOVUYJmYL11y6ooFiMJkJvUZu+xgAc1gZgQe+Px12mZF0CA==",
"requires": {
"@fortawesome/fontawesome-common-types": "^0.2.32"
}
},
"@fortawesome/free-regular-svg-icons": {
"version": "5.15.1",
"resolved": "https://registry.npmjs.org/@fortawesome/free-regular-svg-icons/-/free-regular-svg-icons-5.15.1.tgz",
"integrity": "sha512-eD9NWFy89e7SVVtrLedJUxIpCBGhd4x7s7dhesokjyo1Tw62daqN5UcuAGu1NrepLLq1IeAYUVfWwnOjZ/j3HA==",
"requires": {
"@fortawesome/fontawesome-common-types": "^0.2.32"
}
},
"@fortawesome/free-solid-svg-icons": {
"version": "5.15.1",
"resolved": "https://registry.npmjs.org/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-5.15.1.tgz",
"integrity": "sha512-EFMuKtzRMNbvjab/SvJBaOOpaqJfdSap/Nl6hst7CgrJxwfORR1drdTV6q1Ib/JVzq4xObdTDcT6sqTaXMqfdg==",
"requires": {
"@fortawesome/fontawesome-common-types": "^0.2.32"
}
},
"@istanbuljs/schema": { "@istanbuljs/schema": {
"version": "0.1.2", "version": "0.1.2",
"resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.2.tgz", "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.2.tgz",

View File

@@ -21,6 +21,11 @@
"@angular/platform-browser-dynamic": "~10.1.5", "@angular/platform-browser-dynamic": "~10.1.5",
"@angular/router": "~10.1.5", "@angular/router": "~10.1.5",
"@angular/service-worker": "~10.1.5", "@angular/service-worker": "~10.1.5",
"@fortawesome/angular-fontawesome": "^0.7.0",
"@fortawesome/fontawesome-svg-core": "^1.2.28",
"@fortawesome/free-brands-svg-icons": "^5.13.0",
"@fortawesome/free-regular-svg-icons": "^5.13.0",
"@fortawesome/free-solid-svg-icons": "^5.13.0",
"@types/chart.js": "^2.9.27", "@types/chart.js": "^2.9.27",
"@types/mapbox-gl": "^1.12.5", "@types/mapbox-gl": "^1.12.5",
"@types/moment": "^2.13.0", "@types/moment": "^2.13.0",

View File

@@ -0,0 +1,16 @@
import { TestBed } from '@angular/core/testing';
import { AlertService } from './alert.service';
describe('AlertService', () => {
let service: AlertService;
beforeEach(() => {
TestBed.configureTestingModule({});
service = TestBed.inject(AlertService);
});
it('should be created', () => {
expect(service).toBeTruthy();
});
});

View File

@@ -0,0 +1,64 @@
import { Injectable } from '@angular/core';
import { Observable, Subject } from 'rxjs';
export class Alert {
id: string;
type: AlertType;
message: string;
title?: string;
duration = 5;
isClosing = false;
constructor(init?: Partial<Alert>) {
Object.assign(this, init);
}
}
export enum AlertType {
Success,
Error,
Info,
Warning
}
@Injectable({ providedIn: 'root' })
export class AlertService {
private subject = new Subject<Alert>();
private defaultId = 'default-alert';
// enable subscribing to alerts observable
onAlert(id = this.defaultId): Observable<Alert> {
return this.subject.asObservable();
}
// convenience methods
success(message: string, title = 'Success', options?: any): void {
this.alert(new Alert({ ...options, type: AlertType.Success, message, title }));
}
error(message: string, title = 'Error!', options?: any): void {
this.alert(new Alert({ ...options, type: AlertType.Error, message, title }));
}
info(message: string, title = 'Info', options?: any): void {
this.alert(new Alert({ ...options, type: AlertType.Info, message, title }));
}
warn(message: string, title = 'Warning!', options?: any): void {
this.alert(new Alert({ ...options, type: AlertType.Warning, message, title }));
}
// main alert method
alert(alert: Alert): void {
// Don't show empty alerts
if (alert.message === undefined) { return; }
alert.id = alert.id || this.defaultId;
this.subject.next(alert);
}
// clear alerts
clear(id = this.defaultId): void {
this.subject.next(new Alert({ id }));
}
}

View File

@@ -0,0 +1,5 @@
<div [ngClass]="cssClass(alert)" *ngFor="let alert of this.alerts">
<span class="title">{{alert.title}}</span>
<span class="message">{{alert.message}}</span>
<fa-icon [icon]="faClose" (click)="removeAlert(alert)"></fa-icon>
</div>

View File

@@ -0,0 +1,64 @@
@keyframes appear {
from {
transform: translateY(-100px);
}
to {
transform: translateY(0);
}
}
.alert {
display: flex;
background-color: #000;
width: 100%;
height: 3rem;
padding-left: 1rem;
padding-right: 1rem;
margin-bottom: 1.5rem;
line-height: 0rem;
border-radius: 15px;
vertical-align: middle;
transition: 0.25s ease;
animation: appear 0.5s ease;
box-shadow:
0 4.5px 5.3px rgba(0, 0, 0, 0.121),
0 15px 17.9px rgba(0, 0, 0, 0.179),
0 67px 80px rgba(0, 0, 0, 0.3);
& .title {
font-weight: bolder;
padding-right: 1rem;
white-space: nowrap;
transform: translateY(50%);
}
& .message {
width: 100%;
text-align: center;
transform: translateY(50%);
}
& fa-icon {
transform: translateY(30%);
scale: 1.2;
}
}
.alert-success {
background-color: #0FAE4B !important;
}
.alert-danger {
background-color: #FA2400 !important;
}
.alert-warning {
background-color: #F3CC17 !important;
}
.fadeOut {
transform: scale(0.9);
opacity: 0;
}

View File

@@ -0,0 +1,25 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { AlertComponent } from './alert.component';
describe('AlertComponent', () => {
let component: AlertComponent;
let fixture: ComponentFixture<AlertComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ AlertComponent ]
})
.compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(AlertComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -0,0 +1,75 @@
import { Component, OnInit, OnDestroy, Input } from '@angular/core';
import { Router, NavigationStart } from '@angular/router';
import { faWindowClose } from '@fortawesome/free-solid-svg-icons';
import { Subscription } from 'rxjs';
import { Alert, AlertService, AlertType } from '../alert.service';
/**
* ~~Stoled~~ inspired by https://jasonwatmore.com/post/2020/04/30/angular-9-alert-notifications
*/
// tslint:disable-next-line: component-selector
@Component({ selector: 'alert', templateUrl: 'alert.component.html', styleUrls: ['./alert.component.scss'] })
export class AlertComponent implements OnInit, OnDestroy {
@Input() id = 'default-alert';
// Icons
faClose = faWindowClose;
alerts: Alert[] = [];
alertSubscription: Subscription;
routeSubscription: Subscription;
constructor(private router: Router, private alertService: AlertService) { }
ngOnInit(): void {
this.alertSubscription = this.alertService.onAlert(this.id)
.subscribe(alert => {
// Revert array back to original
this.alerts = this.alerts.reverse();
this.alerts.push(alert);
this.alerts = this.alerts.reverse();
setTimeout(() => {
this.removeAlert(alert);
}, alert.duration * 1000);
});
}
ngOnDestroy(): void {
// unsubscribe to avoid memory leaks
this.alertSubscription.unsubscribe();
this.routeSubscription.unsubscribe();
}
removeAlert(alert: Alert): void {
// check if already removed to prevent error on auto close
if (!this.alerts.includes(alert)) { return; }
alert.isClosing = true;
// remove alert after faded out
setTimeout(() => {
this.alerts = this.alerts.filter(x => x !== alert);
}, 250);
}
cssClass(alert: Alert): string {
if (!alert) { return; }
const classes = ['alert', 'alert-dismissable'];
const alertTypeClass = {
[AlertType.Success]: 'alert alert-success',
[AlertType.Error]: 'alert alert-danger',
[AlertType.Info]: 'alert alert-info',
[AlertType.Warning]: 'alert alert-warning'
};
classes.push(alertTypeClass[alert.type]);
if (alert.isClosing) { classes.push('fadeOut'); }
return classes.join(' ');
}
}

View File

@@ -1,4 +1,5 @@
<h2>Create new user</h2> <h2>Create new user</h2>
<button (click)="alert()">Make info</button>
<form (ngSubmit)="createUser()" #form="ngForm"> <form (ngSubmit)="createUser()" #form="ngForm">
<input name="username" [(ngModel)]="newUsername" placeholder="Username"><br> <input name="username" [(ngModel)]="newUsername" placeholder="Username"><br>
<input name="password" [(ngModel)]="newPassword" type="password" placeholder="Password"><br> <input name="password" [(ngModel)]="newPassword" type="password" placeholder="Password"><br>

View File

@@ -1,5 +1,6 @@
import { AfterContentInit, Component, OnDestroy, OnInit } from '@angular/core'; import { AfterContentInit, Component, OnDestroy, OnInit } from '@angular/core';
import { APIService, UserType } from '../api.service'; import { APIService, UserType } from '../api.service';
import { AlertService } from '../_alert/alert.service';
@Component({ @Component({
selector: 'app-admin', selector: 'app-admin',
@@ -14,7 +15,7 @@ export class AdminComponent implements AfterContentInit, OnDestroy {
newType: UserType; newType: UserType;
invitationCode: string; invitationCode: string;
constructor(public api: APIService) { } constructor(public api: APIService, private alertt: AlertService) { }
ngAfterContentInit(): void { ngAfterContentInit(): void {
this.api.showFilter = false; this.api.showFilter = false;
@@ -29,4 +30,8 @@ export class AdminComponent implements AfterContentInit, OnDestroy {
this.invitationCode = await this.api.createUser(this.newUsername, this.newPassword, this.newType); this.invitationCode = await this.api.createUser(this.newUsername, this.newPassword, this.newType);
} }
alert() {
this.alertt.info('This is a test from admin', 'Admin says');
}
} }

View File

@@ -1,3 +1,5 @@
<alert class="alert-container"></alert>
<div id="header" *ngIf="this.api.loginEvent.value"> <div id="header" *ngIf="this.api.loginEvent.value">
<ul class="navbar"> <ul class="navbar">
<li><a [routerLink]="['/dashboard']" routerLinkActive="router-link-active" >Dashboard</a></li> <li><a [routerLink]="['/dashboard']" routerLinkActive="router-link-active" >Dashboard</a></li>
@@ -10,6 +12,7 @@
<app-filter></app-filter> <app-filter></app-filter>
<router-outlet></router-outlet> <router-outlet></router-outlet>
<div id="loadingOverlay" [ngClass]="{show: this.showOverlay, gone: !this.showOverlay}"> <div id="loadingOverlay" [ngClass]="{show: this.showOverlay, gone: !this.showOverlay}">
<div> <div>
<img src="assets/oval.svg"> <img src="assets/oval.svg">

View File

@@ -1,5 +1,15 @@
@import "../styles.scss"; @import "../styles.scss";
.alert-container {
top: 1rem;
position: fixed;
z-index: 999;
width: 30vw;
min-width: 350px;
height: fit-content;
transform: translateX(35vw);
}
#header { #header {
position: fixed; position: fixed;
top: 0; top: 0;
@@ -19,6 +29,7 @@
& li { & li {
float: left; float: left;
padding-left: 2rem; padding-left: 2rem;
height: 100%;
& a { & a {
text-decoration: none; text-decoration: none;
@@ -38,7 +49,7 @@
display: none; display: none;
top: 30vh; top: 30vh;
left: 0; left: 0;
z-index: 9999; z-index: 999;
backdrop-filter: blur(40px); backdrop-filter: blur(40px);
background-color: rgba(0, 0, 0, 0.75); background-color: rgba(0, 0, 0, 0.75);
width: 100vw; width: 100vw;

View File

@@ -1,6 +1,6 @@
import { Component, OnInit } from '@angular/core'; import { Component, OnInit } from '@angular/core';
import { Router } from '@angular/router';
import { APIService } from './api.service'; import { APIService } from './api.service';
import { AlertService } from './_alert/alert.service';
@Component({ @Component({
selector: 'app-root', selector: 'app-root',
@@ -12,7 +12,7 @@ export class AppComponent implements OnInit{
showOverlay = false; showOverlay = false;
constructor(public api: APIService, private router: Router) { constructor(public api: APIService, private alert: AlertService) {
this.api.fetchingDataEvent.subscribe(status => { this.api.fetchingDataEvent.subscribe(status => {
this.showOverlay = status; this.showOverlay = status;
}); });
@@ -20,6 +20,8 @@ export class AppComponent implements OnInit{
async ngOnInit(): Promise<void> { async ngOnInit(): Promise<void> {
await this.api.login('admin', '$1KDaNCDlyXAOg'); await this.api.login('admin', '$1KDaNCDlyXAOg');
this.alert.success('This is just a test', 'Test');
this.alert.error('Requested user doesn\'t exist', 'Not found');
return; return;
} }
} }

View File

@@ -14,10 +14,12 @@ import { LoginComponent } from './login/login.component';
import { MapComponent } from './map/map.component'; import { MapComponent } from './map/map.component';
import { UserComponent } from './user/user.component'; import { UserComponent } from './user/user.component';
import { DashboardWidgetComponent } from './dashboard-widget/dashboard-widget.component'; import { DashboardWidgetComponent } from './dashboard-widget/dashboard-widget.component';
import { IMqttServiceOptions, MqttModule } from 'ngx-mqtt'; import { MqttModule } from 'ngx-mqtt';
import { ServiceWorkerModule } from '@angular/service-worker'; import { ServiceWorkerModule } from '@angular/service-worker';
import { environment } from '../environments/environment'; import { environment } from '../environments/environment';
import { AdminComponent } from './admin/admin.component'; import { AdminComponent } from './admin/admin.component';
import { AlertComponent } from './_alert/alert/alert.component';
import { FontAwesomeModule } from '@fortawesome/angular-fontawesome';
@NgModule({ @NgModule({
declarations: [ declarations: [
@@ -28,12 +30,14 @@ import { AdminComponent } from './admin/admin.component';
FilterComponent, FilterComponent,
UserComponent, UserComponent,
DashboardWidgetComponent, DashboardWidgetComponent,
AdminComponent AdminComponent,
AlertComponent
], ],
imports: [ imports: [
BrowserModule, BrowserModule,
AppRoutingModule, AppRoutingModule,
BrowserAnimationsModule, BrowserAnimationsModule,
FontAwesomeModule,
FormsModule, FormsModule,
HttpClientModule, HttpClientModule,
MqttModule.forRoot({}), MqttModule.forRoot({}),
@@ -41,7 +45,8 @@ import { AdminComponent } from './admin/admin.component';
accessToken: 'pk.eyJ1IjoibW9uZGVpMSIsImEiOiJja2dsY2ZtaG0xZ2o5MnR0ZWs0Mm82OTBpIn0.NzDWN3P6jJLmci_v3MM1tA' accessToken: 'pk.eyJ1IjoibW9uZGVpMSIsImEiOiJja2dsY2ZtaG0xZ2o5MnR0ZWs0Mm82OTBpIn0.NzDWN3P6jJLmci_v3MM1tA'
}), }),
ChartsModule, ChartsModule,
ServiceWorkerModule.register('ngsw-worker.js', { enabled: environment.production }) ServiceWorkerModule.register('ngsw-worker.js', { enabled: environment.production }),
FontAwesomeModule
], ],
providers: [], providers: [],
bootstrap: [AppComponent] bootstrap: [AppComponent]

View File

@@ -9,8 +9,18 @@
<h1>Battery</h1> <h1>Battery</h1>
<canvas baseChart <canvas baseChart
[chartType]="'line'" [chartType]="'line'"
[datasets]="lineChartData" [datasets]="batteryLineChartData"
[labels]="lineChartLabels" [labels]="batteryLineChartLabels"
[options]="lineChartOptions"
[legend]="false"
></canvas>
</div>
<div class="chartjs-cointainer heightChart">
<h1>Height</h1>
<canvas baseChart
[chartType]="'line'"
[datasets]="heightLineChartData"
[labels]="heightLineChartLabels"
[options]="lineChartOptions" [options]="lineChartOptions"
[legend]="false" [legend]="false"
></canvas> ></canvas>

View File

@@ -13,14 +13,15 @@ export class DashboardComponent implements AfterViewInit {
totalDistance = '0'; totalDistance = '0';
devices = 0; devices = 0;
currentBatteryLevel = 0;
// Array of different segments in chart // Array of different segments in chart
lineChartData: ChartDataSets[] = [ batteryLineChartData: ChartDataSets[] = [
{ data: [], label: 'Battery' } { data: [], label: 'Battery' }
]; ];
// Labels shown on the x-axis // Labels shown on the x-axis
lineChartLabels: Label[] = []; batteryLineChartLabels: Label[] = [];
// Define chart options // Define chart options
lineChartOptions: ChartOptions = { lineChartOptions: ChartOptions = {
@@ -42,31 +43,42 @@ export class DashboardComponent implements AfterViewInit {
} }
}; };
// Array of different segments in chart
heightLineChartData: ChartDataSets[] = [
{ data: [], label: 'Height' }
];
// Labels shown on the x-axis
heightLineChartLabels: Label[] = [];
constructor(public api: APIService) { constructor(public api: APIService) {
this.api.phoneEvent.subscribe(phones => { this.api.phoneEvent.subscribe(phones => {
this.devices = phones.length; this.devices = phones.length;
}); });
this.api.beatsEvent.subscribe(beats => { this.api.beatsEvent.subscribe(beats => {
this.lineChartData[0].data = []; // Only reset array if this is not an update.
this.lineChartLabels = []; if (beats.length !== 1) {
this.batteryLineChartData[0].data = [];
this.batteryLineChartLabels = [];
}
// Filter battery
const batteryLevels: number[] = []; const batteryLevels: number[] = [];
let currentLevel = 0;
const finalBeats = beats.filter((val, i, array) => { const finalBeats = beats.filter((val, i, array) => {
if (currentLevel !== val.battery) { if (this.currentBatteryLevel !== val.battery) {
batteryLevels.push(val.battery); batteryLevels.push(val.battery);
currentLevel = val.battery; this.currentBatteryLevel = val.battery;
return true; return true;
} else { } else {
return false; return false;
} }
}); });
finalBeats.forEach((beat) => { finalBeats.forEach((beat) => {
this.lineChartData[0].data.push(beat.battery); this.batteryLineChartData[0].data.push(beat.battery);
this.lineChartLabels.push(moment(new Date(beat.createdAt)).format(this.lineChartOptions.scales.xAxes[0].time.parser.toString())); this.batteryLineChartLabels.push(moment(new Date(beat.createdAt))
.format(this.lineChartOptions.scales.xAxes[0].time.parser.toString()));
}); });
let tDistance = 0; let tDistance = 0;

View File

@@ -18,9 +18,17 @@ function isPointInRadius(checkPoint: { lat: number, lng: number }, centerPoint:
*/ */
addEventListener('message', ({ data }) => { addEventListener('message', ({ data }) => {
const mostVisit = new Map<string, number>(); const mostVisit = new Map<string, number>();
let progress = 0;
// Get most visit points // Get most visit points
data.forEach(beat => { data.forEach(beat => {
progress++;
// Report progress every fifth loop
if (Math.trunc(progress / data.length * 100) % 5 === 0) {
postMessage({ progress: (progress / data.length) * 100 });
}
// Only if accuracy is low enough // Only if accuracy is low enough
if (beat.accuracy < 35) { if (beat.accuracy < 35) {
data.forEach(beat2 => { data.forEach(beat2 => {

View File

@@ -25,7 +25,7 @@
></mgl-layer> ></mgl-layer>
<div class="controls"> <div class="controls">
<mgl-control *ngIf="heatmapPending"> <mgl-control *ngIf="heatmapPending">
Compute heat map, please wait ... Compute heat map, please wait ... ({{heatmapProgress}}% done)
</mgl-control> </mgl-control>
<mgl-control <mgl-control
mglNavigation mglNavigation

View File

@@ -11,6 +11,7 @@ export class MapComponent {
lastLocation: number[] = [0, 0]; lastLocation: number[] = [0, 0];
showMap = false; showMap = false;
heatmapPending = false; heatmapPending = false;
heatmapProgress = 0;
data: GeoJSON.FeatureCollection<GeoJSON.LineString> = { data: GeoJSON.FeatureCollection<GeoJSON.LineString> = {
type: 'FeatureCollection', features: [ type: 'FeatureCollection', features: [
@@ -162,7 +163,10 @@ export class MapComponent {
// If this is an update don't rebuild entire map. // If this is an update don't rebuild entire map.
if (!isUpdate) { if (!isUpdate) {
console.log('Clear'); if (this.api.beats.length === 0) {
console.warn('Build map method was called while there are no beats!');
return;
}
this.data.features[0].geometry.coordinates = []; this.data.features[0].geometry.coordinates = [];
this.mostVisitData.features[0].geometry.coordinates = []; this.mostVisitData.features[0].geometry.coordinates = [];
@@ -173,7 +177,6 @@ export class MapComponent {
} }
}); });
} else { } else {
console.log('Just push');
if (this.api.getLastBeat().accuracy < maxAccuracy) { if (this.api.getLastBeat().accuracy < maxAccuracy) {
this.data.features[0].geometry.coordinates.push([this.api.getLastBeat().coordinate[1], this.api.getLastBeat().coordinate[0]]); this.data.features[0].geometry.coordinates.push([this.api.getLastBeat().coordinate[1], this.api.getLastBeat().coordinate[0]]);
@@ -181,13 +184,17 @@ export class MapComponent {
} }
this.data = { ... this.data }; this.data = { ... this.data };
console.log('After update / clear:', this.data);
// Let worker compute heatmap // Let worker compute heatmap
if (typeof Worker !== 'undefined') { if (typeof Worker !== 'undefined') {
const worker = new Worker('../map.worker', { type: 'module' }); const worker = new Worker('../map.worker', { type: 'module' });
this.heatmapPending = true; this.heatmapPending = true;
worker.onmessage = ({ data }) => { worker.onmessage = ({ data }) => {
if (data.progress !== undefined) {
this.heatmapProgress = Math.round(data.progress);
return;
}
for (const [key, value] of data) { for (const [key, value] of data) {
if (value < 3) { continue; } if (value < 3) { continue; }

16
package-lock.json generated Normal file
View File

@@ -0,0 +1,16 @@
{
"requires": true,
"lockfileVersion": 1,
"dependencies": {
"angular-font-awesome": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/angular-font-awesome/-/angular-font-awesome-3.1.2.tgz",
"integrity": "sha1-k3hzJhLY6MceDXwvqg+t3H+Fjsk="
},
"font-awesome": {
"version": "4.7.0",
"resolved": "https://registry.npmjs.org/font-awesome/-/font-awesome-4.7.0.tgz",
"integrity": "sha1-j6jPBBGhoxr9B7BtKQK7n8gVoTM="
}
}
}