Notification system in frontend
- Heatmap worker now reports progress - Base for new altitude value - Phones can be marked active
This commit is contained in:
@@ -92,7 +92,10 @@ class TrackerService : Service() {
|
||||
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 jsonAdapter: JsonAdapter<Beat> = moshi.adapter(Beat::class.java)
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ import { Phone } from "../models/phone/phone.model";
|
||||
export async function GetPhone(req: LivebeatRequest, res: Response) {
|
||||
const phoneId: String = req.params['id'];
|
||||
|
||||
// If none id provided, return all.
|
||||
// If no id provided, return all.
|
||||
if (phoneId === undefined) {
|
||||
const phone = await Phone.find({ user: req.user?._id });
|
||||
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 });
|
||||
|
||||
if (phone !== null) {
|
||||
logger.debug("Request to /phone failed because phone already exists.");
|
||||
res.status(409).send();
|
||||
res.status(409).send({ message: "This phone already exists." });
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -60,7 +59,8 @@ export async function PostPhone(req: LivebeatRequest, res: Response) {
|
||||
modelName,
|
||||
operatingSystem,
|
||||
architecture,
|
||||
user: req.user?._id
|
||||
user: req.user?._id,
|
||||
active: false
|
||||
});
|
||||
|
||||
logger.info(`New device (${displayName}) registered for ${req.user?.name}.`)
|
||||
|
||||
@@ -30,13 +30,14 @@ export class RabbitMQ {
|
||||
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({
|
||||
phone: phone._id,
|
||||
coordinate: [msg.gpsLocation[0], msg.gpsLocation[1]],
|
||||
accuracy: msg.gpsLocation[2],
|
||||
speed: msg.gpsLocation[3],
|
||||
// [latitude, longitude, altitude]
|
||||
coordinate: [msg.gpsLocation[0], msg.gpsLocation[1], msg.gpsLocation[2]],
|
||||
accuracy: msg.gpsLocation[3],
|
||||
speed: msg.gpsLocation[4],
|
||||
battery: msg.battery,
|
||||
createdAt: msg.timestamp
|
||||
});
|
||||
|
||||
@@ -2,6 +2,7 @@ import { Document } from 'mongoose';
|
||||
import { IPhone } from '../phone/phone.interface';
|
||||
|
||||
export interface IBeat extends Document {
|
||||
// [latitude, longitude, altitude, accuracy, speed]
|
||||
coordinate?: number[],
|
||||
accuracy: number,
|
||||
speed: number,
|
||||
|
||||
@@ -8,6 +8,7 @@ export interface IPhone extends Document {
|
||||
operatingSystem: String,
|
||||
architecture: String,
|
||||
user: IUser,
|
||||
active: Boolean,
|
||||
updatedAt?: Date,
|
||||
createdAt?: Date
|
||||
}
|
||||
@@ -7,7 +7,8 @@ const schemaPhone = new Schema({
|
||||
modelName: { type: String, required: false },
|
||||
operatingSystem: { 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: {
|
||||
createdAt: true,
|
||||
|
||||
@@ -31,7 +31,8 @@
|
||||
"styles": [
|
||||
"src/styles.scss",
|
||||
"./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": [
|
||||
"./node_modules/chart.js/dist/Chart.min.js"
|
||||
|
||||
45
frontend/package-lock.json
generated
45
frontend/package-lock.json
generated
@@ -1653,6 +1653,51 @@
|
||||
"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": {
|
||||
"version": "0.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.2.tgz",
|
||||
|
||||
@@ -21,6 +21,11 @@
|
||||
"@angular/platform-browser-dynamic": "~10.1.5",
|
||||
"@angular/router": "~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/mapbox-gl": "^1.12.5",
|
||||
"@types/moment": "^2.13.0",
|
||||
|
||||
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,22 +43,32 @@ 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) {
|
||||
if (this.currentBatteryLevel !== val.battery) {
|
||||
batteryLevels.push(val.battery);
|
||||
currentLevel = val.battery;
|
||||
this.currentBatteryLevel = val.battery;
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
@@ -65,8 +76,9 @@ export class DashboardComponent implements AfterViewInit {
|
||||
});
|
||||
|
||||
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; }
|
||||
|
||||
|
||||
16
package-lock.json
generated
Normal file
16
package-lock.json
generated
Normal 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="
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user