Heatmap solver is now a worker
- Android app now requests root (purpose still unknown) - Android app starts on boot - Frontend is now a PWA (purpose still unknown)
This commit is contained in:
1
frontend/src/app/admin/admin.component.html
Normal file
1
frontend/src/app/admin/admin.component.html
Normal file
@@ -0,0 +1 @@
|
||||
<p>admin works!</p>
|
||||
0
frontend/src/app/admin/admin.component.scss
Normal file
0
frontend/src/app/admin/admin.component.scss
Normal file
25
frontend/src/app/admin/admin.component.spec.ts
Normal file
25
frontend/src/app/admin/admin.component.spec.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { AdminComponent } from './admin.component';
|
||||
|
||||
describe('AdminComponent', () => {
|
||||
let component: AdminComponent;
|
||||
let fixture: ComponentFixture<AdminComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [ AdminComponent ]
|
||||
})
|
||||
.compileComponents();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(AdminComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
22
frontend/src/app/admin/admin.component.ts
Normal file
22
frontend/src/app/admin/admin.component.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import { AfterContentInit, Component, OnDestroy, OnInit } from '@angular/core';
|
||||
import { APIService } from '../api.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-admin',
|
||||
templateUrl: './admin.component.html',
|
||||
styleUrls: ['./admin.component.scss']
|
||||
})
|
||||
export class AdminComponent implements AfterContentInit, OnDestroy {
|
||||
|
||||
constructor(public api: APIService) { }
|
||||
|
||||
ngAfterContentInit(): void {
|
||||
this.api.showFilter = false;
|
||||
console.log(this.api.showFilter);
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.api.showFilter = true;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -103,7 +103,28 @@ export class APIService {
|
||||
|
||||
API_ENDPOINT = 'http://192.168.178.26:8040';
|
||||
|
||||
constructor(private httpClient: HttpClient, private mqtt: MqttService) {}
|
||||
constructor(private httpClient: HttpClient, private mqtt: MqttService) { }
|
||||
|
||||
private mqttInit(): void {
|
||||
// Connect with RabbitMQ after we received our user information
|
||||
this.mqtt.connect({
|
||||
hostname: '192.168.178.26',
|
||||
port: 15675,
|
||||
protocol: 'ws',
|
||||
path: '/ws',
|
||||
username: this.user.name,
|
||||
password: this.user.brokerToken
|
||||
});
|
||||
|
||||
this.mqtt.observe('/').subscribe(message => {
|
||||
if (this.beats !== undefined) {
|
||||
const obj = JSON.parse(message.payload.toString()) as IBeat;
|
||||
this.beats.push(obj);
|
||||
this.beatsEvent.next([obj]); // We just push one, so all the map doesn't has to rebuild the entire map.
|
||||
this.beatStats.totalBeats++;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async login(username: string, password: string): Promise<ILogin> {
|
||||
return new Promise<ILogin>(async (resolve, reject) => {
|
||||
@@ -116,23 +137,7 @@ export class APIService {
|
||||
await this.getPhones();
|
||||
await this.getUserInfo();
|
||||
|
||||
// Connect with RabbitMQ after we received our user information
|
||||
this.mqtt.connect({
|
||||
hostname: '192.168.178.26',
|
||||
port: 15675,
|
||||
protocol: 'ws',
|
||||
path: '/ws',
|
||||
username: this.user.name,
|
||||
password: this.user.brokerToken
|
||||
});
|
||||
|
||||
this.mqtt.observe('/').subscribe(message => {
|
||||
if (this.beats !== undefined) {
|
||||
this.beats.push(JSON.parse(message.payload.toString()) as IBeat);
|
||||
this.beatsEvent.next(this.beats);
|
||||
this.beatStats.totalBeats++;
|
||||
}
|
||||
});
|
||||
this.mqttInit();
|
||||
|
||||
await this.getBeats();
|
||||
await this.getBeatStats();
|
||||
@@ -233,7 +238,7 @@ export class APIService {
|
||||
this.httpClient.get(this.API_ENDPOINT + '/phone/' + phoneId, { responseType: 'json', headers })
|
||||
.subscribe(phones => {
|
||||
this.fetchingDataEvent.next(false);
|
||||
resolve(phones as {IPhone, IBeat});
|
||||
resolve(phones as { IPhone, IBeat });
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -257,12 +262,21 @@ export class APIService {
|
||||
lat2 = this.degreesToRadians(lat2);
|
||||
|
||||
const a = Math.sin(dLat / 2) * Math.sin(dLat / 2) +
|
||||
Math.sin(dLon / 2) * Math.sin(dLon / 2) * Math.cos(lat1) * Math.cos(lat2);
|
||||
Math.sin(dLon / 2) * Math.sin(dLon / 2) * Math.cos(lat1) * Math.cos(lat2);
|
||||
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
|
||||
|
||||
return earthRadiusKm * c;
|
||||
}
|
||||
|
||||
/**
|
||||
* Short form for `this.api.beats[this.api.beats.length - 1]`
|
||||
*
|
||||
* **Notice:** This does not fetch new beats, instead use cached `this.beats`
|
||||
*/
|
||||
getLastBeat(): IBeat {
|
||||
return this.beats[this.beats.length - 1];
|
||||
}
|
||||
|
||||
hasSession(): boolean {
|
||||
return this.token !== undefined;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { Routes, RouterModule } from '@angular/router';
|
||||
import { AdminComponent } from './admin/admin.component';
|
||||
import { AppComponent } from './app.component';
|
||||
import { DashboardComponent } from './dashboard/dashboard.component';
|
||||
import { LoginComponent } from './login/login.component';
|
||||
@@ -26,6 +27,10 @@ const routes: Routes = [
|
||||
{
|
||||
path: 'user',
|
||||
component: UserComponent
|
||||
},
|
||||
{
|
||||
path: 'admin',
|
||||
component: AdminComponent
|
||||
}
|
||||
];
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<li><a [routerLink]="['/dashboard']" routerLinkActive="router-link-active" >Dashboard</a></li>
|
||||
<li><a [routerLink]="['/map']" routerLinkActive="router-link-active" >Map</a></li>
|
||||
<li class="navbar-right"><a [routerLink]="['/user']" routerLinkActive="router-link-active" >{{this.api.username}}</a></li>
|
||||
<li class="navbar-right"><a [routerLink]="['/settings']" routerLinkActive="router-link-active" *ngIf="this.api.user.type == 'admin'">Admin settings</a></li>
|
||||
<li class="navbar-right"><a [routerLink]="['/admin']" routerLinkActive="router-link-active" *ngIf="this.api.user.type == 'admin'">Admin settings</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="header-spacer"></div>
|
||||
|
||||
@@ -15,6 +15,9 @@ 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 { ServiceWorkerModule } from '@angular/service-worker';
|
||||
import { environment } from '../environments/environment';
|
||||
import { AdminComponent } from './admin/admin.component';
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
@@ -24,7 +27,8 @@ import { IMqttServiceOptions, MqttModule } from 'ngx-mqtt';
|
||||
MapComponent,
|
||||
FilterComponent,
|
||||
UserComponent,
|
||||
DashboardWidgetComponent
|
||||
DashboardWidgetComponent,
|
||||
AdminComponent
|
||||
],
|
||||
imports: [
|
||||
BrowserModule,
|
||||
@@ -36,7 +40,8 @@ import { IMqttServiceOptions, MqttModule } from 'ngx-mqtt';
|
||||
NgxMapboxGLModule.withConfig({
|
||||
accessToken: 'pk.eyJ1IjoibW9uZGVpMSIsImEiOiJja2dsY2ZtaG0xZ2o5MnR0ZWs0Mm82OTBpIn0.NzDWN3P6jJLmci_v3MM1tA'
|
||||
}),
|
||||
ChartsModule
|
||||
ChartsModule,
|
||||
ServiceWorkerModule.register('ngsw-worker.js', { enabled: environment.production })
|
||||
],
|
||||
providers: [],
|
||||
bootstrap: [AppComponent]
|
||||
|
||||
46
frontend/src/app/map.worker.ts
Normal file
46
frontend/src/app/map.worker.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
/// <reference lib="webworker" />
|
||||
|
||||
/* Function to find out if a provided point is in the area from
|
||||
https://stackoverflow.com/questions/24680247/check-if-a-latitude-and-longitude-is-within-a-circle-google-maps
|
||||
*/
|
||||
function isPointInRadius(checkPoint: { lat: number, lng: number }, centerPoint: { lat: number, lng: number }, km: number): boolean {
|
||||
const ky = 40000 / 360;
|
||||
const kx = Math.cos(Math.PI * centerPoint.lat / 180.0) * ky;
|
||||
const dx = Math.abs(centerPoint.lng - checkPoint.lng) * kx;
|
||||
const dy = Math.abs(centerPoint.lat - checkPoint.lat) * ky;
|
||||
return Math.sqrt(dx * dx + dy * dy) <= km;
|
||||
}
|
||||
|
||||
/**
|
||||
* This web worker computes the heatmap. Since this process takes a while for thousends of objects
|
||||
* we compute them in there own thread.
|
||||
* @param data Is an array of all beats
|
||||
*/
|
||||
addEventListener('message', ({ data }) => {
|
||||
const mostVisit = new Map<string, number>();
|
||||
|
||||
// Get most visit points
|
||||
data.forEach(beat => {
|
||||
// Only if accuracy is low enough
|
||||
if (beat.accuracy < 35) {
|
||||
data.forEach(beat2 => {
|
||||
const isNearPoint = isPointInRadius(
|
||||
{ lat: beat2.coordinate[0], lng: beat2.coordinate[1] },
|
||||
{ lat: beat.coordinate[0], lng: beat.coordinate[1] },
|
||||
0.025
|
||||
);
|
||||
|
||||
if (isNearPoint) {
|
||||
if (mostVisit.has(beat2._id)) {
|
||||
mostVisit.set(beat2._id, mostVisit.get(beat2._id) + 1);
|
||||
} else {
|
||||
mostVisit.set(beat2._id, 1);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
console.log(`worker response to`, mostVisit);
|
||||
postMessage(mostVisit);
|
||||
});
|
||||
@@ -23,4 +23,17 @@
|
||||
source="lastLoc"
|
||||
[paint]="lastLocationPaint"
|
||||
></mgl-layer>
|
||||
<div class="controls">
|
||||
<mgl-control *ngIf="heatmapPending">
|
||||
Compute heat map, please wait ...
|
||||
</mgl-control>
|
||||
<mgl-control
|
||||
mglNavigation
|
||||
></mgl-control>
|
||||
<mgl-control
|
||||
mglScale
|
||||
unit="metric"
|
||||
position="top-right"
|
||||
></mgl-control>
|
||||
</div>
|
||||
</mgl-map>
|
||||
@@ -10,6 +10,7 @@ export class MapComponent {
|
||||
|
||||
lastLocation: number[] = [0, 0];
|
||||
showMap = false;
|
||||
heatmapPending = false;
|
||||
|
||||
data: GeoJSON.FeatureCollection<GeoJSON.LineString> = {
|
||||
type: 'FeatureCollection', features: [
|
||||
@@ -126,15 +127,16 @@ export class MapComponent {
|
||||
if (beats.length === 0) { return; }
|
||||
|
||||
this.update();
|
||||
this.buildMap(beats.length === 1);
|
||||
|
||||
this.lastLocationPaint['circle-radius'].stops[1][1] = this.metersToPixelsAtMaxZoom(
|
||||
beats[beats.length - 1].accuracy, this.lastLocation[0]
|
||||
);
|
||||
this.lastLocationPaint = { ...this.lastLocationPaint };
|
||||
});
|
||||
|
||||
this.api.maxAccuracy.subscribe(val => {
|
||||
this.buildMap(val);
|
||||
});
|
||||
this.api.maxAccuracy.subscribe(val => {
|
||||
this.buildMap(false, val);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -145,22 +147,7 @@ export class MapComponent {
|
||||
return meters / 0.075 / Math.cos(latitude * Math.PI / 180);
|
||||
}
|
||||
|
||||
/* Function to find out if a provided point is in the area from
|
||||
https://stackoverflow.com/questions/24680247/check-if-a-latitude-and-longitude-is-within-a-circle-google-maps
|
||||
*/
|
||||
private isPointInRadius(checkPoint: { lat: number, lng: number }, centerPoint: { lat: number, lng: number }, km: number): boolean {
|
||||
const ky = 40000 / 360;
|
||||
const kx = Math.cos(Math.PI * centerPoint.lat / 180.0) * ky;
|
||||
const dx = Math.abs(centerPoint.lng - checkPoint.lng) * kx;
|
||||
const dy = Math.abs(centerPoint.lat - checkPoint.lat) * ky;
|
||||
return Math.sqrt(dx * dx + dy * dy) <= km;
|
||||
}
|
||||
|
||||
async update(): Promise<void> {
|
||||
this.data.features[0].geometry.coordinates = [];
|
||||
|
||||
this.buildMap();
|
||||
|
||||
this.lastLocation = [this.api.beats[this.api.beats.length - 1].coordinate[1],
|
||||
this.api.beats[this.api.beats.length - 1].coordinate[0]];
|
||||
|
||||
@@ -170,46 +157,56 @@ export class MapComponent {
|
||||
this.showMap = true;
|
||||
}
|
||||
|
||||
buildMap(maxAccuracy: number = 30): void {
|
||||
const mostVisit = new Map<string, number>();
|
||||
this.data.features[0].geometry.coordinates = [];
|
||||
this.mostVisitData.features[0].geometry.coordinates = [];
|
||||
buildMap(isUpdate: boolean, maxAccuracy: number = 30): void {
|
||||
console.log(isUpdate);
|
||||
|
||||
for (let i = 0; i < this.api.beats.length; i++) {
|
||||
const beat = this.api.beats[i];
|
||||
if (beat.accuracy > maxAccuracy) { continue; }
|
||||
this.data.features[0].geometry.coordinates.push([beat.coordinate[1], beat.coordinate[0]]);
|
||||
// If this is an update don't rebuild entire map.
|
||||
if (!isUpdate) {
|
||||
console.log('Clear');
|
||||
|
||||
// Get most visit points
|
||||
for (let b = 0; b < this.api.beats.length; b++) {
|
||||
const beat2 = this.api.beats[b];
|
||||
const isNearPoint = this.isPointInRadius(
|
||||
{ lat: beat2.coordinate[0], lng: beat2.coordinate[1] },
|
||||
{ lat: beat.coordinate[0], lng: beat.coordinate[1] },
|
||||
0.02
|
||||
);
|
||||
this.data.features[0].geometry.coordinates = [];
|
||||
this.mostVisitData.features[0].geometry.coordinates = [];
|
||||
|
||||
if (isNearPoint) {
|
||||
if (mostVisit.has(beat2._id)) {
|
||||
mostVisit.set(beat2._id, mostVisit.get(beat2._id) + 1);
|
||||
} else {
|
||||
mostVisit.set(beat2._id, 1);
|
||||
}
|
||||
this.api.beats.forEach(beat => {
|
||||
if (beat.accuracy < maxAccuracy) {
|
||||
this.data.features[0].geometry.coordinates.push([beat.coordinate[1], beat.coordinate[0]]);
|
||||
}
|
||||
});
|
||||
} 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]]);
|
||||
}
|
||||
}
|
||||
|
||||
for (const [key, value] of mostVisit) {
|
||||
if (value < 2) { continue; }
|
||||
this.api.beats.forEach(beat => {
|
||||
if (beat._id === key) {
|
||||
this.mostVisitData.features[0].geometry.coordinates.push([beat.coordinate[1], beat.coordinate[0]]);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
this.data = { ... this.data };
|
||||
this.mostVisitData = { ... this.mostVisitData };
|
||||
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 }) => {
|
||||
for (const [key, value] of data) {
|
||||
if (value < 3) { continue; }
|
||||
|
||||
// Find beat with id
|
||||
this.api.beats.forEach(beat => {
|
||||
if (beat._id === key) {
|
||||
this.mostVisitData.features[0].geometry.coordinates.push([beat.coordinate[1], beat.coordinate[0]]);
|
||||
}
|
||||
});
|
||||
}
|
||||
worker.terminate();
|
||||
this.heatmapPending = false;
|
||||
this.mostVisitData = { ... this.mostVisitData };
|
||||
};
|
||||
|
||||
worker.postMessage(isUpdate ? [this.api.getLastBeat()] : this.api.beats);
|
||||
} else {
|
||||
// TODO: Support older browsers
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user