Notification system in frontend
- Heatmap worker now reports progress - Base for new altitude value - Phones can be marked active
This commit is contained in:
16
frontend/src/app/_alert/alert.service.spec.ts
Normal file
16
frontend/src/app/_alert/alert.service.spec.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
64
frontend/src/app/_alert/alert.service.ts
Normal file
64
frontend/src/app/_alert/alert.service.ts
Normal 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 }));
|
||||
}
|
||||
}
|
||||
5
frontend/src/app/_alert/alert/alert.component.html
Normal file
5
frontend/src/app/_alert/alert/alert.component.html
Normal 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>
|
||||
64
frontend/src/app/_alert/alert/alert.component.scss
Normal file
64
frontend/src/app/_alert/alert/alert.component.scss
Normal 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;
|
||||
}
|
||||
25
frontend/src/app/_alert/alert/alert.component.spec.ts
Normal file
25
frontend/src/app/_alert/alert/alert.component.spec.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
75
frontend/src/app/_alert/alert/alert.component.ts
Normal file
75
frontend/src/app/_alert/alert/alert.component.ts
Normal 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(' ');
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
<h2>Create new user</h2>
|
||||
<button (click)="alert()">Make info</button>
|
||||
<form (ngSubmit)="createUser()" #form="ngForm">
|
||||
<input name="username" [(ngModel)]="newUsername" placeholder="Username"><br>
|
||||
<input name="password" [(ngModel)]="newPassword" type="password" placeholder="Password"><br>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { AfterContentInit, Component, OnDestroy, OnInit } from '@angular/core';
|
||||
import { APIService, UserType } from '../api.service';
|
||||
import { AlertService } from '../_alert/alert.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-admin',
|
||||
@@ -14,7 +15,7 @@ export class AdminComponent implements AfterContentInit, OnDestroy {
|
||||
newType: UserType;
|
||||
invitationCode: string;
|
||||
|
||||
constructor(public api: APIService) { }
|
||||
constructor(public api: APIService, private alertt: AlertService) { }
|
||||
|
||||
ngAfterContentInit(): void {
|
||||
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);
|
||||
}
|
||||
|
||||
alert() {
|
||||
this.alertt.info('This is a test from admin', 'Admin says');
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
<alert class="alert-container"></alert>
|
||||
|
||||
<div id="header" *ngIf="this.api.loginEvent.value">
|
||||
<ul class="navbar">
|
||||
<li><a [routerLink]="['/dashboard']" routerLinkActive="router-link-active" >Dashboard</a></li>
|
||||
@@ -10,6 +12,7 @@
|
||||
<app-filter></app-filter>
|
||||
|
||||
<router-outlet></router-outlet>
|
||||
|
||||
<div id="loadingOverlay" [ngClass]="{show: this.showOverlay, gone: !this.showOverlay}">
|
||||
<div>
|
||||
<img src="assets/oval.svg">
|
||||
|
||||
@@ -1,5 +1,15 @@
|
||||
@import "../styles.scss";
|
||||
|
||||
.alert-container {
|
||||
top: 1rem;
|
||||
position: fixed;
|
||||
z-index: 999;
|
||||
width: 30vw;
|
||||
min-width: 350px;
|
||||
height: fit-content;
|
||||
transform: translateX(35vw);
|
||||
}
|
||||
|
||||
#header {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
@@ -19,6 +29,7 @@
|
||||
& li {
|
||||
float: left;
|
||||
padding-left: 2rem;
|
||||
height: 100%;
|
||||
|
||||
& a {
|
||||
text-decoration: none;
|
||||
@@ -38,7 +49,7 @@
|
||||
display: none;
|
||||
top: 30vh;
|
||||
left: 0;
|
||||
z-index: 9999;
|
||||
z-index: 999;
|
||||
backdrop-filter: blur(40px);
|
||||
background-color: rgba(0, 0, 0, 0.75);
|
||||
width: 100vw;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { Router } from '@angular/router';
|
||||
import { APIService } from './api.service';
|
||||
import { AlertService } from './_alert/alert.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-root',
|
||||
@@ -12,7 +12,7 @@ export class AppComponent implements OnInit{
|
||||
|
||||
showOverlay = false;
|
||||
|
||||
constructor(public api: APIService, private router: Router) {
|
||||
constructor(public api: APIService, private alert: AlertService) {
|
||||
this.api.fetchingDataEvent.subscribe(status => {
|
||||
this.showOverlay = status;
|
||||
});
|
||||
@@ -20,6 +20,8 @@ export class AppComponent implements OnInit{
|
||||
|
||||
async ngOnInit(): Promise<void> {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,10 +14,12 @@ 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';
|
||||
import { MqttModule } from 'ngx-mqtt';
|
||||
import { ServiceWorkerModule } from '@angular/service-worker';
|
||||
import { environment } from '../environments/environment';
|
||||
import { AdminComponent } from './admin/admin.component';
|
||||
import { AlertComponent } from './_alert/alert/alert.component';
|
||||
import { FontAwesomeModule } from '@fortawesome/angular-fontawesome';
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
@@ -28,12 +30,14 @@ import { AdminComponent } from './admin/admin.component';
|
||||
FilterComponent,
|
||||
UserComponent,
|
||||
DashboardWidgetComponent,
|
||||
AdminComponent
|
||||
AdminComponent,
|
||||
AlertComponent
|
||||
],
|
||||
imports: [
|
||||
BrowserModule,
|
||||
AppRoutingModule,
|
||||
BrowserAnimationsModule,
|
||||
FontAwesomeModule,
|
||||
FormsModule,
|
||||
HttpClientModule,
|
||||
MqttModule.forRoot({}),
|
||||
@@ -41,7 +45,8 @@ import { AdminComponent } from './admin/admin.component';
|
||||
accessToken: 'pk.eyJ1IjoibW9uZGVpMSIsImEiOiJja2dsY2ZtaG0xZ2o5MnR0ZWs0Mm82OTBpIn0.NzDWN3P6jJLmci_v3MM1tA'
|
||||
}),
|
||||
ChartsModule,
|
||||
ServiceWorkerModule.register('ngsw-worker.js', { enabled: environment.production })
|
||||
ServiceWorkerModule.register('ngsw-worker.js', { enabled: environment.production }),
|
||||
FontAwesomeModule
|
||||
],
|
||||
providers: [],
|
||||
bootstrap: [AppComponent]
|
||||
|
||||
@@ -9,8 +9,18 @@
|
||||
<h1>Battery</h1>
|
||||
<canvas baseChart
|
||||
[chartType]="'line'"
|
||||
[datasets]="lineChartData"
|
||||
[labels]="lineChartLabels"
|
||||
[datasets]="batteryLineChartData"
|
||||
[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"
|
||||
[legend]="false"
|
||||
></canvas>
|
||||
|
||||
@@ -13,14 +13,15 @@ export class DashboardComponent implements AfterViewInit {
|
||||
|
||||
totalDistance = '0';
|
||||
devices = 0;
|
||||
currentBatteryLevel = 0;
|
||||
|
||||
// Array of different segments in chart
|
||||
lineChartData: ChartDataSets[] = [
|
||||
batteryLineChartData: ChartDataSets[] = [
|
||||
{ data: [], label: 'Battery' }
|
||||
];
|
||||
|
||||
// Labels shown on the x-axis
|
||||
lineChartLabels: Label[] = [];
|
||||
batteryLineChartLabels: Label[] = [];
|
||||
|
||||
// Define chart options
|
||||
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) {
|
||||
this.api.phoneEvent.subscribe(phones => {
|
||||
this.devices = phones.length;
|
||||
});
|
||||
|
||||
this.api.beatsEvent.subscribe(beats => {
|
||||
this.lineChartData[0].data = [];
|
||||
this.lineChartLabels = [];
|
||||
// Only reset array if this is not an update.
|
||||
if (beats.length !== 1) {
|
||||
this.batteryLineChartData[0].data = [];
|
||||
this.batteryLineChartLabels = [];
|
||||
}
|
||||
|
||||
// Filter battery
|
||||
const batteryLevels: number[] = [];
|
||||
let currentLevel = 0;
|
||||
|
||||
const finalBeats = beats.filter((val, i, array) => {
|
||||
if (currentLevel !== val.battery) {
|
||||
batteryLevels.push(val.battery);
|
||||
currentLevel = val.battery;
|
||||
return true;
|
||||
if (this.currentBatteryLevel !== val.battery) {
|
||||
batteryLevels.push(val.battery);
|
||||
this.currentBatteryLevel = val.battery;
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
finalBeats.forEach((beat) => {
|
||||
this.lineChartData[0].data.push(beat.battery);
|
||||
this.lineChartLabels.push(moment(new Date(beat.createdAt)).format(this.lineChartOptions.scales.xAxes[0].time.parser.toString()));
|
||||
this.batteryLineChartData[0].data.push(beat.battery);
|
||||
this.batteryLineChartLabels.push(moment(new Date(beat.createdAt))
|
||||
.format(this.lineChartOptions.scales.xAxes[0].time.parser.toString()));
|
||||
});
|
||||
|
||||
let tDistance = 0;
|
||||
|
||||
@@ -18,9 +18,17 @@ function isPointInRadius(checkPoint: { lat: number, lng: number }, centerPoint:
|
||||
*/
|
||||
addEventListener('message', ({ data }) => {
|
||||
const mostVisit = new Map<string, number>();
|
||||
let progress = 0;
|
||||
|
||||
// Get most visit points
|
||||
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
|
||||
if (beat.accuracy < 35) {
|
||||
data.forEach(beat2 => {
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
></mgl-layer>
|
||||
<div class="controls">
|
||||
<mgl-control *ngIf="heatmapPending">
|
||||
Compute heat map, please wait ...
|
||||
Compute heat map, please wait ... ({{heatmapProgress}}% done)
|
||||
</mgl-control>
|
||||
<mgl-control
|
||||
mglNavigation
|
||||
|
||||
@@ -11,6 +11,7 @@ export class MapComponent {
|
||||
lastLocation: number[] = [0, 0];
|
||||
showMap = false;
|
||||
heatmapPending = false;
|
||||
heatmapProgress = 0;
|
||||
|
||||
data: GeoJSON.FeatureCollection<GeoJSON.LineString> = {
|
||||
type: 'FeatureCollection', features: [
|
||||
@@ -162,7 +163,10 @@ export class MapComponent {
|
||||
|
||||
// If this is an update don't rebuild entire map.
|
||||
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.mostVisitData.features[0].geometry.coordinates = [];
|
||||
@@ -173,7 +177,6 @@ export class MapComponent {
|
||||
}
|
||||
});
|
||||
} else {
|
||||
console.log('Just push');
|
||||
|
||||
if (this.api.getLastBeat().accuracy < maxAccuracy) {
|
||||
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 };
|
||||
console.log('After update / clear:', this.data);
|
||||
|
||||
// Let worker compute heatmap
|
||||
if (typeof Worker !== 'undefined') {
|
||||
const worker = new Worker('../map.worker', { type: 'module' });
|
||||
this.heatmapPending = true;
|
||||
worker.onmessage = ({ data }) => {
|
||||
if (data.progress !== undefined) {
|
||||
this.heatmapProgress = Math.round(data.progress);
|
||||
return;
|
||||
}
|
||||
|
||||
for (const [key, value] of data) {
|
||||
if (value < 3) { continue; }
|
||||
|
||||
|
||||
Reference in New Issue
Block a user