Real-time communication with frontend
- Frontend shows heatmap of most visit places - Maximum accuracy can now be set - Fix bug where battery chart filtered values wrongly
This commit is contained in:
@@ -1,12 +1,15 @@
|
||||
import { HttpClient, HttpHeaders, HttpParams } from '@angular/common/http';
|
||||
import { Injectable } from '@angular/core';
|
||||
import { MqttService } from 'ngx-mqtt';
|
||||
import { BehaviorSubject } from 'rxjs';
|
||||
import * as moment from 'moment';
|
||||
|
||||
export interface ILogin {
|
||||
token: string;
|
||||
}
|
||||
|
||||
export interface IBeat {
|
||||
_id: string;
|
||||
coordinate?: number[];
|
||||
accuracy: number;
|
||||
speed: number;
|
||||
@@ -35,8 +38,7 @@ export enum UserType {
|
||||
|
||||
export interface IUser {
|
||||
name: string;
|
||||
password: string;
|
||||
salt: string;
|
||||
brokerToken: string;
|
||||
type: UserType;
|
||||
lastLogin: Date;
|
||||
twoFASecret?: string;
|
||||
@@ -67,8 +69,15 @@ export class APIService {
|
||||
private token: string;
|
||||
|
||||
username: string;
|
||||
time: ITimespan | undefined;
|
||||
rabbitmq: any;
|
||||
time: ITimespan | undefined = {
|
||||
from: moment().subtract(1, 'day').unix(),
|
||||
to: moment().unix()
|
||||
};
|
||||
|
||||
// Passthough data (not useful for api but a way for components to share data)
|
||||
showFilter = true;
|
||||
maxAccuracy: BehaviorSubject<number> = new BehaviorSubject(30);
|
||||
|
||||
// Cached data
|
||||
beats: IBeat[];
|
||||
@@ -79,9 +88,8 @@ export class APIService {
|
||||
phones: IPhone[];
|
||||
user: IUser = {
|
||||
name: '',
|
||||
brokerToken: '',
|
||||
lastLogin: new Date(2020, 3, 1),
|
||||
password: '',
|
||||
salt: '',
|
||||
type: UserType.GUEST,
|
||||
createdAt: new Date(),
|
||||
twoFASecret: ''
|
||||
@@ -95,7 +103,7 @@ export class APIService {
|
||||
|
||||
API_ENDPOINT = 'http://192.168.178.26:8040';
|
||||
|
||||
constructor(private httpClient: HttpClient) { }
|
||||
constructor(private httpClient: HttpClient, private mqtt: MqttService) {}
|
||||
|
||||
async login(username: string, password: string): Promise<ILogin> {
|
||||
return new Promise<ILogin>(async (resolve, reject) => {
|
||||
@@ -107,6 +115,25 @@ export class APIService {
|
||||
this.username = username;
|
||||
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++;
|
||||
}
|
||||
});
|
||||
|
||||
await this.getBeats();
|
||||
await this.getBeatStats();
|
||||
this.loginEvent.next(true);
|
||||
|
||||
@@ -14,6 +14,7 @@ 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';
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
@@ -31,6 +32,7 @@ import { DashboardWidgetComponent } from './dashboard-widget/dashboard-widget.co
|
||||
BrowserAnimationsModule,
|
||||
FormsModule,
|
||||
HttpClientModule,
|
||||
MqttModule.forRoot({}),
|
||||
NgxMapboxGLModule.withConfig({
|
||||
accessToken: 'pk.eyJ1IjoibW9uZGVpMSIsImEiOiJja2dsY2ZtaG0xZ2o5MnR0ZWs0Mm82OTBpIn0.NzDWN3P6jJLmci_v3MM1tA'
|
||||
}),
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { AfterViewInit, Component, OnInit } from '@angular/core';
|
||||
import { ChartDataSets, ChartOptions } from 'chart.js';
|
||||
import { Label } from 'ng2-charts';
|
||||
import * as moment from 'moment';
|
||||
import { APIService, IBeat } from '../api.service';
|
||||
|
||||
@Component({
|
||||
@@ -51,10 +52,12 @@ export class DashboardComponent implements AfterViewInit {
|
||||
this.lineChartLabels = [];
|
||||
|
||||
const batteryLevels: number[] = [];
|
||||
let currentLevel = 0;
|
||||
|
||||
const finalBeats = beats.filter((val, i, array) => {
|
||||
if (batteryLevels.indexOf(val.battery) === -1) {
|
||||
if (currentLevel !== val.battery) {
|
||||
batteryLevels.push(val.battery);
|
||||
currentLevel = val.battery;
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
@@ -63,7 +66,7 @@ export class DashboardComponent implements AfterViewInit {
|
||||
|
||||
finalBeats.forEach((beat) => {
|
||||
this.lineChartData[0].data.push(beat.battery);
|
||||
this.lineChartLabels.push(this.formatDateTime(new Date(beat.createdAt)));
|
||||
this.lineChartLabels.push(moment(new Date(beat.createdAt)).format(this.lineChartOptions.scales.xAxes[0].time.parser.toString()));
|
||||
});
|
||||
|
||||
let tDistance = 0;
|
||||
@@ -83,10 +86,6 @@ export class DashboardComponent implements AfterViewInit {
|
||||
});
|
||||
}
|
||||
|
||||
private formatDateTime(date: Date): string {
|
||||
return `${date.getMonth()}/${date.getDay()}/${date.getFullYear()} ${date.getHours()}:${date.getMinutes()}:${date.getSeconds()}`;
|
||||
}
|
||||
|
||||
ngAfterViewInit(): void {
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
<div id="filter" [ngClass]="{hide: !this.api.showFilter}" *ngIf="this.api.loginEvent.value">
|
||||
<h3 (click)="update()" style="cursor: pointer;">Refresh</h3>
|
||||
|
||||
<!-- Time range -->
|
||||
<select (change)="update($event.target.value)" [(ngModel)]="this.presetHours">
|
||||
<option value="-1">Today</option>
|
||||
<option value="3">Last 3h</option>
|
||||
@@ -17,4 +19,8 @@
|
||||
<option value="month">Months</option>
|
||||
<option value="year">Years</option>
|
||||
</select>
|
||||
|
||||
<!-- Max accuracy -->
|
||||
<h4>Max accuracy</h4>
|
||||
<input class="customRange" (change)="updateAccuracy($event)">
|
||||
</div>
|
||||
@@ -40,6 +40,10 @@ export class FilterComponent implements OnInit {
|
||||
this.refresh();
|
||||
}
|
||||
|
||||
updateAccuracy(val: any): void {
|
||||
this.api.maxAccuracy.next(Number(val.target.value));
|
||||
}
|
||||
|
||||
async refresh(): Promise<void> {
|
||||
await this.api.getBeats();
|
||||
await this.api.getBeatStats();
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<mgl-map [style]="'mapbox://styles/mapbox/dark-v10'" [zoom]="[15]" [center]="[this.lastLocation[0], this.lastLocation[1]]" *ngIf="showMap">
|
||||
<mgl-geojson-source id="locHistory" [data]="data"></mgl-geojson-source>
|
||||
<mgl-geojson-source id="locHistoryFiltered" [data]="mostVisitData"></mgl-geojson-source>
|
||||
<mgl-geojson-source id="lastLoc" [data]="lastLocationData"></mgl-geojson-source>
|
||||
<mgl-layer
|
||||
id="locHistory"
|
||||
@@ -10,6 +11,12 @@
|
||||
'line-width': 3
|
||||
}"
|
||||
></mgl-layer>
|
||||
<mgl-layer
|
||||
id="locHistoryHeatmap"
|
||||
type="heatmap"
|
||||
source="locHistoryFiltered"
|
||||
[paint]="mostVisitPaint"
|
||||
></mgl-layer>
|
||||
<mgl-layer
|
||||
id="lastLoc"
|
||||
type="circle"
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { AfterViewInit, Component, OnInit } from '@angular/core';
|
||||
import { Map } from 'mapbox-gl';
|
||||
import { APIService } from '../api.service';
|
||||
|
||||
@Component({
|
||||
@@ -7,9 +6,8 @@ import { APIService } from '../api.service';
|
||||
templateUrl: './map.component.html',
|
||||
styleUrls: ['./map.component.scss']
|
||||
})
|
||||
export class MapComponent implements AfterViewInit {
|
||||
export class MapComponent {
|
||||
|
||||
map: Map;
|
||||
lastLocation: number[] = [0, 0];
|
||||
showMap = false;
|
||||
|
||||
@@ -21,6 +19,79 @@ export class MapComponent implements AfterViewInit {
|
||||
geometry: { type: 'LineString', coordinates: [] }
|
||||
}]
|
||||
};
|
||||
mostVisitData: GeoJSON.FeatureCollection<GeoJSON.LineString> = {
|
||||
type: 'FeatureCollection',
|
||||
features: [{
|
||||
type: 'Feature',
|
||||
properties: null,
|
||||
geometry: { type: 'LineString', coordinates: [] }
|
||||
}]
|
||||
};
|
||||
mostVisitPaint = {
|
||||
// Increase the heatmap weight based on frequency and property magnitude
|
||||
'heatmap-weight': [
|
||||
'interpolate',
|
||||
['linear'],
|
||||
['get', 'mag'],
|
||||
0,
|
||||
0,
|
||||
6,
|
||||
1
|
||||
],
|
||||
// Increase the heatmap color weight weight by zoom level
|
||||
// heatmap-intensity is a multiplier on top of heatmap-weight
|
||||
'heatmap-intensity': [
|
||||
'interpolate',
|
||||
['linear'],
|
||||
['zoom'],
|
||||
0,
|
||||
3,
|
||||
9,
|
||||
1
|
||||
],
|
||||
// Color ramp for heatmap. Domain is 0 (low) to 1 (high).
|
||||
// Begin color ramp at 0-stop with a 0-transparancy color
|
||||
// to create a blur-like effect.
|
||||
'heatmap-color': [
|
||||
'interpolate',
|
||||
['linear'],
|
||||
['heatmap-density'],
|
||||
0,
|
||||
'rgba(33,102,172,0)',
|
||||
0.3,
|
||||
'rgb(103,169,207)',
|
||||
0.4,
|
||||
'rgb(209,229,240)',
|
||||
0.7,
|
||||
'rgb(253,219,199)',
|
||||
0.95,
|
||||
'rgb(239,138,98)',
|
||||
1,
|
||||
'rgb(178,24,43)'
|
||||
],
|
||||
// Adjust the heatmap radius by zoom level
|
||||
'heatmap-radius': [
|
||||
'interpolate',
|
||||
['linear'],
|
||||
['zoom'],
|
||||
0,
|
||||
2,
|
||||
7,
|
||||
10,
|
||||
9,
|
||||
15
|
||||
],
|
||||
// Transition from heatmap to circle layer by zoom level
|
||||
'heatmap-opacity': [
|
||||
'interpolate',
|
||||
['linear'],
|
||||
['zoom'],
|
||||
16,
|
||||
1,
|
||||
17,
|
||||
0
|
||||
]
|
||||
};
|
||||
|
||||
lastLocationData: GeoJSON.FeatureCollection<GeoJSON.Point> = {
|
||||
type: 'FeatureCollection', features: [
|
||||
@@ -54,33 +125,44 @@ export class MapComponent implements AfterViewInit {
|
||||
this.api.beatsEvent.subscribe(beats => {
|
||||
if (beats.length === 0) { return; }
|
||||
|
||||
this.lastLocationPaint['circle-radius'].stops[1][1] = this.metersToPixelsAtMaxZoom(
|
||||
beats[0].accuracy, this.lastLocation[0]
|
||||
);
|
||||
this.update();
|
||||
|
||||
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);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/* Function to draw circle with exact size by
|
||||
/* Function to draw circle with exact size from
|
||||
https://stackoverflow.com/questions/37599561/drawing-a-circle-with-the-radius-in-miles-meters-with-mapbox-gl-js
|
||||
*/
|
||||
private metersToPixelsAtMaxZoom(meters: number, latitude: number): number {
|
||||
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 = [];
|
||||
|
||||
// Add lines to map backwards (because it looks cool)
|
||||
for (let i = this.api.beats.length - 1; i >= 0; i--) {
|
||||
const beat = this.api.beats[i];
|
||||
this.data.features[0].geometry.coordinates.push([beat.coordinate[1], beat.coordinate[0]]);
|
||||
this.data = { ... this.data };
|
||||
}
|
||||
console.log('Last', this.api.beats[0]);
|
||||
this.buildMap();
|
||||
|
||||
this.lastLocation = [ this.api.beats[0].coordinate[1],
|
||||
this.api.beats[0].coordinate[0] ];
|
||||
this.lastLocation = [this.api.beats[this.api.beats.length - 1].coordinate[1],
|
||||
this.api.beats[this.api.beats.length - 1].coordinate[0]];
|
||||
|
||||
this.lastLocationData.features[0].geometry.coordinates = this.lastLocation;
|
||||
this.lastLocationData = { ...this.lastLocationData };
|
||||
@@ -88,7 +170,46 @@ export class MapComponent implements AfterViewInit {
|
||||
this.showMap = true;
|
||||
}
|
||||
|
||||
async ngAfterViewInit(): Promise<void> {
|
||||
buildMap(maxAccuracy: number = 30): void {
|
||||
const mostVisit = new Map<string, number>();
|
||||
this.data.features[0].geometry.coordinates = [];
|
||||
this.mostVisitData.features[0].geometry.coordinates = [];
|
||||
|
||||
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]]);
|
||||
|
||||
// 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
|
||||
);
|
||||
|
||||
if (isNearPoint) {
|
||||
if (mostVisit.has(beat2._id)) {
|
||||
mostVisit.set(beat2._id, mostVisit.get(beat2._id) + 1);
|
||||
} else {
|
||||
mostVisit.set(beat2._id, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 };
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
@import '../../styles.scss';
|
||||
|
||||
#user {
|
||||
min-width: 40rem;
|
||||
margin-top: 3rem;
|
||||
margin-left: 20rem;
|
||||
margin-right: 20rem;
|
||||
|
||||
Reference in New Issue
Block a user