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:
2020-10-28 15:06:21 +01:00
parent 056195a188
commit b3b3d9d9c4
30 changed files with 357 additions and 78 deletions

View File

@@ -0,0 +1 @@
<p>admin works!</p>

View 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();
});
});

View 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;
}
}

View File

@@ -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;
}

View File

@@ -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
}
];

View File

@@ -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>

View File

@@ -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]

View 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);
});

View File

@@ -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>

View File

@@ -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
}
}
}