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()
|
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)
|
||||||
|
|
||||||
|
|||||||
@@ -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}.`)
|
||||||
|
|||||||
@@ -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
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
@@ -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,
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
45
frontend/package-lock.json
generated
45
frontend/package-lock.json
generated
@@ -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",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
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>
|
<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>
|
||||||
|
|||||||
@@ -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');
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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]
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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,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) {
|
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;
|
||||||
@@ -65,8 +76,9 @@ export class DashboardComponent implements AfterViewInit {
|
|||||||
});
|
});
|
||||||
|
|
||||||
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;
|
||||||
|
|||||||
@@ -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 => {
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
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