Frontend can now filter with timespan

This commit is contained in:
2020-10-23 22:08:54 +02:00
parent 13f8437f29
commit edaff8a3ec
29 changed files with 558 additions and 152 deletions

View File

@@ -92,6 +92,7 @@ async function run() {
/** /**
* Database connection * Database connection
*/ */
mongoose.set('debug', true);
const connection = await mongoose.connect(MONGO_URI, { useNewUrlParser: true, useUnifiedTopology: true }).catch((err) => { const connection = await mongoose.connect(MONGO_URI, { useNewUrlParser: true, useUnifiedTopology: true }).catch((err) => {
logger.crit("Database connection could not be made: ", err); logger.crit("Database connection could not be made: ", err);
exit(1); exit(1);

View File

@@ -1,30 +1,56 @@
import { Response } from "express"; import { Response } from "express";
import { logger } from "../app";
import { LivebeatRequest } from "../lib/request"; import { LivebeatRequest } from "../lib/request";
import { IBeat } from "../models/beat/beat.interface";
import { Beat } from "../models/beat/beat.model."; import { Beat } from "../models/beat/beat.model.";
import { Phone } from "../models/phone/phone.model"; import { Phone } from "../models/phone/phone.model";
export interface IFilter {
phone: string,
time: {
from: number,
to: number
},
max: number
}
export async function GetBeat(req: LivebeatRequest, res: Response) { export async function GetBeat(req: LivebeatRequest, res: Response) {
const filter: IFilter = req.body.filter as IFilter; const from: number = Number(req.query.from);
const to: number = Number(req.query.to);
const preset: string = req.query.preset as string;
const phoneId = req.query.phoneId;
// If no filters are specified, we return the last 500 points. We take the first phone as default. const phone = req.query.phone === undefined ? await Phone.findOne({ user: req.user?._id }) : await Phone.findOne({ _id: phoneId, user: req.user?._id });
if (filter === undefined) { let beats: IBeat[] = []
const phone = await Phone.findOne({ user: req.user?._id });
logger.debug(`No filters were provided! Take ${phone?.displayName} as default.`);
if (phone !== undefined && phone !== null) { if (phone !== null) {
logger.debug("Query for latest beats ..."); // If the battery preset is chosen, we only return documents where the battery status changed.
const beats = await Beat.find({ phone: phone._id }).limit(800).sort({ _id: -1 }); if (preset === 'battery') {
res.status(200).send(beats); // Group documents under the battery percentage.
const batteryBeats = await Beat.aggregate([
{ $group: { _id: {battery: "$battery"}, uniqueIds: { $addToSet: "$_id" } } }
]).sort({ '_id.battery': -1 }).sort({ '_id.id': -1 });
// Loop though array and grab the first document where the battery percentage changed.
for (let i = 0; i < batteryBeats.length; i++) {
const firstId = batteryBeats[i].uniqueIds[0];
const firstBeat = await Beat.findById(firstId);
if (firstBeat !== null) {
beats.push(firstBeat);
} }
} }
} else {
// If no preset is chosen, get latest.
beats = await Beat.find({ phone: phone._id, createdAt: { $gte: new Date((from | 0) * 1000), $lte: new Date((to | Date.now() / 1000) * 1000) } }).sort({ _id: -1 });
}
// Check time
/*for (let i = 0; i < beats.length; i++) {
const beat = beats[i];
if (!isNaN(from) && !isNaN(to)) {
if (Math.floor(beat.createdAt!.getTime() / 1000) >= new Date(from).getTime() &&
Math.floor(beat.createdAt!.getTime() / 1000) <= new Date(to).getTime()) {}
else {
console.log(`${Math.floor(beat.createdAt!.getTime() / 1000)} is not in range`);
beats.splice(i, 1);
}
}
}*/
res.status(200).send(beats);
} else {
res.status(404).send({ message: 'Phone not found' });
}
} }

View File

@@ -6,7 +6,8 @@ const schemaBeat = new Schema({
accuracy: { type: Number, required: false }, accuracy: { type: Number, required: false },
speed: { type: Number, required: false }, speed: { type: Number, required: false },
battery: { type: Number, required: false }, battery: { type: Number, required: false },
phone: { type: SchemaTypes.ObjectId, required: true, default: 'user' } phone: { type: SchemaTypes.ObjectId, required: true, default: 'user' },
createdAt: { type: SchemaTypes.Date, required: false }
}, { }, {
timestamps: { timestamps: {
createdAt: true createdAt: true

View File

@@ -158,6 +158,15 @@
"integrity": "sha512-Jus9s4CDbqwocc5pOAnh8ShfrnMcPHuJYzVcSUU7lrh8Ni5HuIqX3oilL86p3dlTrk0LzHRCgA/GQ7uNCw6l2Q==", "integrity": "sha512-Jus9s4CDbqwocc5pOAnh8ShfrnMcPHuJYzVcSUU7lrh8Ni5HuIqX3oilL86p3dlTrk0LzHRCgA/GQ7uNCw6l2Q==",
"dev": true "dev": true
}, },
"@types/moment": {
"version": "2.13.0",
"resolved": "https://registry.npmjs.org/@types/moment/-/moment-2.13.0.tgz",
"integrity": "sha1-YE69GJvDvDShVIaJQE5hoqSqyJY=",
"dev": true,
"requires": {
"moment": "*"
}
},
"@types/mongodb": { "@types/mongodb": {
"version": "3.5.28", "version": "3.5.28",
"resolved": "https://registry.npmjs.org/@types/mongodb/-/mongodb-3.5.28.tgz", "resolved": "https://registry.npmjs.org/@types/mongodb/-/mongodb-3.5.28.tgz",
@@ -1728,6 +1737,11 @@
"minimist": "^1.2.5" "minimist": "^1.2.5"
} }
}, },
"moment": {
"version": "2.29.1",
"resolved": "https://registry.npmjs.org/moment/-/moment-2.29.1.tgz",
"integrity": "sha512-kHmoybcPV8Sqy59DwNDY3Jefr64lK/by/da0ViFcuA4DH0vQg5Q6Ze5VimxkfQNSC+Mls/Kx53s7TjP1RhFEDQ=="
},
"mongodb": { "mongodb": {
"version": "3.6.2", "version": "3.6.2",
"resolved": "https://registry.npmjs.org/mongodb/-/mongodb-3.6.2.tgz", "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-3.6.2.tgz",

View File

@@ -26,6 +26,7 @@
"express": "^4.17.1", "express": "^4.17.1",
"figlet": "^1.5.0", "figlet": "^1.5.0",
"jsonwebtoken": "^8.5.1", "jsonwebtoken": "^8.5.1",
"moment": "^2.29.1",
"mongoose": "^5.10.9", "mongoose": "^5.10.9",
"ts-node": "^9.0.0", "ts-node": "^9.0.0",
"typescript": "^4.0.3", "typescript": "^4.0.3",
@@ -45,6 +46,7 @@
"nodemon": "^2.0.5", "nodemon": "^2.0.5",
"@types/jsonwebtoken": "8.5.0", "@types/jsonwebtoken": "8.5.0",
"@types/amqplib": "0.5.14", "@types/amqplib": "0.5.14",
"@types/cors": "2.8.8" "@types/cors": "2.8.8",
"@types/moment": "2.13.0"
} }
} }

View File

@@ -32,7 +32,9 @@
"./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"
], ],
"scripts": [] "scripts": [
"./node_modules/chart.js/dist/Chart.min.js"
]
}, },
"configurations": { "configurations": {
"production": { "production": {

View File

@@ -1798,24 +1798,6 @@
"resolved": "https://registry.npmjs.org/@mapbox/whoots-js/-/whoots-js-3.1.0.tgz", "resolved": "https://registry.npmjs.org/@mapbox/whoots-js/-/whoots-js-3.1.0.tgz",
"integrity": "sha512-Es6WcD0nO5l+2BOQS4uLfNPYQaNDfbot3X1XUoloz+x0mPDS3eeORZJl06HXjwBG1fOGwCRnzK88LMdxKRrd6Q==" "integrity": "sha512-Es6WcD0nO5l+2BOQS4uLfNPYQaNDfbot3X1XUoloz+x0mPDS3eeORZJl06HXjwBG1fOGwCRnzK88LMdxKRrd6Q=="
}, },
"@nebular/auth": {
"version": "6.2.1",
"resolved": "https://registry.npmjs.org/@nebular/auth/-/auth-6.2.1.tgz",
"integrity": "sha512-s2xiyT5zUxVxz3UULZCjbHpaouPjAfUhPJKjQ5kl92YLIyj1PqFC5+ONiJh27i92rb90iuO0OatvrH//jSoZUA=="
},
"@nebular/eva-icons": {
"version": "6.2.1",
"resolved": "https://registry.npmjs.org/@nebular/eva-icons/-/eva-icons-6.2.1.tgz",
"integrity": "sha512-YoZqHpSy9VPy/MiAczOeclEOtwyhKA0HrsXMIInUbzc5vmC2xFcVsYNXi9S48DufgANDLjH3I61CiOp11ynyRw=="
},
"@nebular/theme": {
"version": "6.2.1",
"resolved": "https://registry.npmjs.org/@nebular/theme/-/theme-6.2.1.tgz",
"integrity": "sha512-0lknv6t8IY7l05/G8d/9OFfkNVjISYoTFzIWAmGQPvBL+MlQIoOstAOSlTmWNcJP/KdHvr3iUXupR2P3bTv4Ow==",
"requires": {
"intersection-observer": "0.7.0"
}
},
"@ngtools/webpack": { "@ngtools/webpack": {
"version": "10.1.6", "version": "10.1.6",
"resolved": "https://registry.npmjs.org/@ngtools/webpack/-/webpack-10.1.6.tgz", "resolved": "https://registry.npmjs.org/@ngtools/webpack/-/webpack-10.1.6.tgz",
@@ -1934,6 +1916,14 @@
"resolved": "https://registry.npmjs.org/@sinonjs/text-encoding/-/text-encoding-0.7.1.tgz", "resolved": "https://registry.npmjs.org/@sinonjs/text-encoding/-/text-encoding-0.7.1.tgz",
"integrity": "sha512-+iTbntw2IZPb/anVDbypzfQa+ay64MW0Zo8aJ8gZPWMMK6/OubMVb6lUPMagqjOPnmtauXnFCACVl3O7ogjeqQ==" "integrity": "sha512-+iTbntw2IZPb/anVDbypzfQa+ay64MW0Zo8aJ8gZPWMMK6/OubMVb6lUPMagqjOPnmtauXnFCACVl3O7ogjeqQ=="
}, },
"@types/chart.js": {
"version": "2.9.27",
"resolved": "https://registry.npmjs.org/@types/chart.js/-/chart.js-2.9.27.tgz",
"integrity": "sha512-b3ho2RpPLWzLzOXKkFwpvlRDEVWQrCknu2/p90mLY5v2DO8owk0OwWkv4MqAC91kJL52bQGXkVw/De+N/0/1+A==",
"requires": {
"moment": "^2.10.2"
}
},
"@types/geojson": { "@types/geojson": {
"version": "7946.0.7", "version": "7946.0.7",
"resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.7.tgz", "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.7.tgz",
@@ -3340,6 +3330,32 @@
"integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==", "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==",
"dev": true "dev": true
}, },
"chart.js": {
"version": "2.9.4",
"resolved": "https://registry.npmjs.org/chart.js/-/chart.js-2.9.4.tgz",
"integrity": "sha512-B07aAzxcrikjAPyV+01j7BmOpxtQETxTSlQ26BEYJ+3iUkbNKaOJ/nDbT6JjyqYxseM0ON12COHYdU2cTIjC7A==",
"requires": {
"chartjs-color": "^2.1.0",
"moment": "^2.10.2"
}
},
"chartjs-color": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/chartjs-color/-/chartjs-color-2.4.1.tgz",
"integrity": "sha512-haqOg1+Yebys/Ts/9bLo/BqUcONQOdr/hoEr2LLTRl6C5LXctUdHxsCYfvQVg5JIxITrfCNUDr4ntqmQk9+/0w==",
"requires": {
"chartjs-color-string": "^0.6.0",
"color-convert": "^1.9.3"
}
},
"chartjs-color-string": {
"version": "0.6.0",
"resolved": "https://registry.npmjs.org/chartjs-color-string/-/chartjs-color-string-0.6.0.tgz",
"integrity": "sha512-TIB5OKn1hPJvO7JcteW4WY/63v6KwEdt6udfnDE9iCAZgy+V4SrbSxoIbTw/xkUIapjEI4ExGtD0+6D3KyFd7A==",
"requires": {
"color-name": "^1.0.0"
}
},
"chokidar": { "chokidar": {
"version": "3.4.2", "version": "3.4.2",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.4.2.tgz", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.4.2.tgz",
@@ -6595,11 +6611,6 @@
"ipaddr.js": "^1.9.0" "ipaddr.js": "^1.9.0"
} }
}, },
"intersection-observer": {
"version": "0.7.0",
"resolved": "https://registry.npmjs.org/intersection-observer/-/intersection-observer-0.7.0.tgz",
"integrity": "sha512-Id0Fij0HsB/vKWGeBe9PxeY45ttRiBmhFyyt/geBdDHBYNctMRTE3dC1U3ujzz3lap+hVXlEcVaB56kZP/eEUg=="
},
"into-stream": { "into-stream": {
"version": "3.1.0", "version": "3.1.0",
"resolved": "https://registry.npmjs.org/into-stream/-/into-stream-3.1.0.tgz", "resolved": "https://registry.npmjs.org/into-stream/-/into-stream-3.1.0.tgz",
@@ -7669,6 +7680,11 @@
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz",
"integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==" "integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA=="
}, },
"lodash-es": {
"version": "4.17.15",
"resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.15.tgz",
"integrity": "sha512-rlrc3yU3+JNOpZ9zj5pQtxnx2THmvRykwL4Xlxoa8I9lHBlVbbyPhgyPMioxVZ4NqyxaVVtaJnzsyOidQIhyyQ=="
},
"lodash.clonedeep": { "lodash.clonedeep": {
"version": "4.5.0", "version": "4.5.0",
"resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz", "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz",
@@ -8336,6 +8352,11 @@
"minimist": "^1.2.5" "minimist": "^1.2.5"
} }
}, },
"moment": {
"version": "2.29.1",
"resolved": "https://registry.npmjs.org/moment/-/moment-2.29.1.tgz",
"integrity": "sha512-kHmoybcPV8Sqy59DwNDY3Jefr64lK/by/da0ViFcuA4DH0vQg5Q6Ze5VimxkfQNSC+Mls/Kx53s7TjP1RhFEDQ=="
},
"move-concurrently": { "move-concurrently": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/move-concurrently/-/move-concurrently-1.0.1.tgz", "resolved": "https://registry.npmjs.org/move-concurrently/-/move-concurrently-1.0.1.tgz",
@@ -8438,6 +8459,16 @@
"integrity": "sha1-yobR/ogoFpsBICCOPchCS524NCw=", "integrity": "sha1-yobR/ogoFpsBICCOPchCS524NCw=",
"dev": true "dev": true
}, },
"ng2-charts": {
"version": "2.4.2",
"resolved": "https://registry.npmjs.org/ng2-charts/-/ng2-charts-2.4.2.tgz",
"integrity": "sha512-mY3C2uKCaApHCQizS2YxEOqQ7sSZZLxdV6N1uM9u/VvUgVtYvlPtdcXbKpN52ak93ZE22I73DiLWVDnDNG4/AQ==",
"requires": {
"@types/chart.js": "^2.9.24",
"lodash-es": "^4.17.15",
"tslib": "^2.0.0"
}
},
"ngx-mapbox-gl": { "ngx-mapbox-gl": {
"version": "4.8.1", "version": "4.8.1",
"resolved": "https://registry.npmjs.org/ngx-mapbox-gl/-/ngx-mapbox-gl-4.8.1.tgz", "resolved": "https://registry.npmjs.org/ngx-mapbox-gl/-/ngx-mapbox-gl-4.8.1.tgz",

View File

@@ -20,13 +20,13 @@
"@angular/platform-browser": "~10.1.5", "@angular/platform-browser": "~10.1.5",
"@angular/platform-browser-dynamic": "~10.1.5", "@angular/platform-browser-dynamic": "~10.1.5",
"@angular/router": "~10.1.5", "@angular/router": "~10.1.5",
"@nebular/auth": "^6.2.1", "@types/chart.js": "^2.9.27",
"@nebular/eva-icons": "^6.2.1",
"@nebular/theme": "^6.2.1",
"@types/mapbox-gl": "^1.12.5", "@types/mapbox-gl": "^1.12.5",
"chart.js": "^2.9.4",
"eva-icons": "^1.1.3", "eva-icons": "^1.1.3",
"geojson": "^0.5.0", "geojson": "^0.5.0",
"mapbox-gl": "^1.12.0", "mapbox-gl": "^1.12.0",
"ng2-charts": "^2.4.2",
"ngx-mapbox-gl": "^4.8.1", "ngx-mapbox-gl": "^4.8.1",
"rxjs": "~6.6.0", "rxjs": "~6.6.0",
"tslib": "^2.0.0", "tslib": "^2.0.0",

View File

@@ -1,5 +1,6 @@
import { HttpClient, HttpHeaders } from '@angular/common/http'; import { HttpClient, HttpHeaders, HttpParams } from '@angular/common/http';
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { BehaviorSubject } from 'rxjs';
export interface ILogin { export interface ILogin {
token: string; token: string;
@@ -14,6 +15,11 @@ export interface IBeat {
createdAt?: Date; createdAt?: Date;
} }
export interface ITimespan {
from?: number;
to?: number;
}
@Injectable({ @Injectable({
providedIn: 'root' providedIn: 'root'
}) })
@@ -22,8 +28,17 @@ export class APIService {
private token: string; private token: string;
username: string; username: string;
time: ITimespan | undefined;
API_ENDPOINT = 'http://192.168.178.26:8040' // Cached data
beats: IBeat[];
// Events when new data got fetched
beatsEvent: BehaviorSubject<IBeat[]> = new BehaviorSubject([]);
loginEvent: BehaviorSubject<boolean> = new BehaviorSubject(false);
fetchingDataEvent: BehaviorSubject<boolean> = new BehaviorSubject(false);
API_ENDPOINT = 'http://192.168.178.26:8040';
constructor(private httpClient: HttpClient) { } constructor(private httpClient: HttpClient) { }
@@ -37,20 +52,34 @@ export class APIService {
this.token = (token as ILogin).token; this.token = (token as ILogin).token;
this.username = username; this.username = username;
this.loginEvent.next(true);
resolve(token as ILogin); resolve(token as ILogin);
}); });
}); });
} }
async getBeats(): Promise<IBeat[]> { async getBeats(preset?: 'battery'): Promise<IBeat[]> {
return new Promise<IBeat[]>((resolve, reject) => { return new Promise<IBeat[]>((resolve, reject) => {
if (this.token === undefined) { reject([]); } if (this.token === undefined) { reject([]); }
const headers = new HttpHeaders({ token: this.token }); this.fetchingDataEvent.next(true);
this.httpClient.get(this.API_ENDPOINT + '/beat', { responseType: 'json', headers }) const headers = new HttpHeaders({ token: this.token });
let params = new HttpParams()
.set('preset', preset);
if (this.time !== undefined) {
params = params.set('from', this.time.from.toString());
params = params.set('to', this.time.to.toString());
}
console.log(params);
this.httpClient.get(this.API_ENDPOINT + '/beat', { responseType: 'json', headers, params })
.subscribe(beats => { .subscribe(beats => {
console.log(beats); this.beats = beats as IBeat[];
this.beatsEvent.next(beats as IBeat[]);
this.fetchingDataEvent.next(false);
resolve(beats as IBeat[]); resolve(beats as IBeat[]);
}); });
}); });

View File

@@ -1,9 +1,9 @@
import { NgModule } from '@angular/core'; import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router'; import { Routes, RouterModule } from '@angular/router';
import { NbAuthComponent, NbLoginComponent } from '@nebular/auth';
import { AppComponent } from './app.component'; import { AppComponent } from './app.component';
import { DashboardComponent } from './dashboard/dashboard.component'; import { DashboardComponent } from './dashboard/dashboard.component';
import { LoginComponent } from './login/login.component'; import { LoginComponent } from './login/login.component';
import { MapComponent } from './map/map.component';
const routes: Routes = [ const routes: Routes = [
{ {
@@ -17,6 +17,10 @@ const routes: Routes = [
{ {
path: 'dashboard', path: 'dashboard',
component: DashboardComponent component: DashboardComponent
},
{
path: 'map',
component: MapComponent
} }
]; ];

View File

@@ -1,12 +1,16 @@
<div id="header"> <div id="header">
<p>Header</p> <ul class="navbar">
<div class="left"> <li><a [routerLink]="['/dashboard']" routerLinkActive="router-link-active" >Dashboard</a></li>
<span class="ident" *ngIf="this.api.hasSession()">Logged in as {{this.api.username}}</span> <li><a [routerLink]="['/map']" routerLinkActive="router-link-active" >Map</a></li>
<li><a [routerLink]="['/settings']" routerLinkActive="router-link-active" >Settings</a></li>
<li class="navbar-right"><a href="#about">{{this.api.username}}</a></li>
</ul>
</div>
<div class="header-spacer"></div>
<app-filter></app-filter>
<router-outlet></router-outlet>
<div id="loadingOverlay" [ngClass]="{show: this.showOverlay, gone: !this.showOverlay}">
<div>
<img src="assets/oval.svg">
</div> </div>
</div> </div>
<router-outlet></router-outlet>
<!-- Display start page -->
<div id="startpage" *ngIf="false">
<h1>Livebeat</h1>
</div>

View File

@@ -1,10 +1,76 @@
#header { #header {
position: fixed;
top: 0;
left: 0;
width: 100vw; width: 100vw;
height: fit-content; height: fit-content;
padding-top: 0.4rem; padding-top: 0.8rem;
padding-bottom: 0.4rem; padding-bottom: 0.8rem;
background-color: #1d1d1dd9; background-color: #1d1d1dd9;
backdrop-filter: blur(20px); backdrop-filter: blur(30px);
box-shadow: 10px 10px 50px 0px rgba(0,0,0,0.85); box-shadow: 10px 10px 50px 0px rgba(0,0,0,0.85);
& ul {
display: inline;
list-style: none;
& li {
float: left;
padding-left: 2rem;
& a {
text-decoration: none;
color: white;
}
}
& .navbar-right {
float: right !important;
padding-right: 2rem;
}
}
}
#loadingOverlay {
position: fixed;
display: none;
top: 30vh;
left: 0;
z-index: 9999;
backdrop-filter: blur(40px);
background-color: rgba(0,0,0,0.75);
width: 100vw;
height: 30vh;
vertical-align: middle;
align-items: center;
& div {
height: fit-content;
margin: 0 auto;
& img {
width: 5rem;
}
}
}
.show {
display: flex !important;
animation: showOverlay .2s ease-in-out;
}
@keyframes showOverlay {
from {
backdrop-filter: blur(0px);
background-color: rgba(0,0,0,0);
}
to {
backdrop-filter: blur(10px);
background-color: rgba(0,0,0,0.8);
}
}
.header-spacer {
height: 3rem;
} }

View File

@@ -7,9 +7,19 @@ import { APIService } from './api.service';
templateUrl: './app.component.html', templateUrl: './app.component.html',
styleUrls: ['./app.component.scss'], styleUrls: ['./app.component.scss'],
}) })
export class AppComponent { export class AppComponent implements OnInit{
title = 'Livebeat'; title = 'Livebeat';
showOverlay = false;
constructor(public api: APIService, private router: Router) { constructor(public api: APIService, private router: Router) {
this.api.fetchingDataEvent.subscribe(status => {
this.showOverlay = status;
});
}
async ngOnInit(): Promise<void> {
await this.api.login('admin', '$1KDaNCDlyXAOg');
return;
} }
} }

View File

@@ -1,23 +1,25 @@
import { HttpClientModule } from '@angular/common/http'; import { HttpClientModule } from '@angular/common/http';
import { NgModule } from '@angular/core'; import { NgModule } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { BrowserModule } from '@angular/platform-browser'; import { BrowserModule } from '@angular/platform-browser';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { NbEvaIconsModule } from '@nebular/eva-icons'; import { ChartsModule } from 'ng2-charts';
import { NbPasswordAuthStrategy, NbAuthModule, NbDummyAuthStrategy } from '@nebular/auth'; import { NgxMapboxGLModule } from 'ngx-mapbox-gl';
import { NbCardModule, NbLayoutModule, NbSidebarModule, NbThemeModule } from '@nebular/theme';
import { AppRoutingModule } from './app-routing.module'; import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component'; import { AppComponent } from './app.component';
import { LoginComponent } from './login/login.component';
import { FormsModule } from '@angular/forms';
import { DashboardComponent } from './dashboard/dashboard.component'; import { DashboardComponent } from './dashboard/dashboard.component';
import { NgxMapboxGLModule } from 'ngx-mapbox-gl'; import { FilterComponent } from './filter/filter.component';
import { LoginComponent } from './login/login.component';
import { MapComponent } from './map/map.component';
@NgModule({ @NgModule({
declarations: [ declarations: [
AppComponent, AppComponent,
LoginComponent, LoginComponent,
DashboardComponent DashboardComponent,
MapComponent,
FilterComponent
], ],
imports: [ imports: [
BrowserModule, BrowserModule,
@@ -28,21 +30,7 @@ import { NgxMapboxGLModule } from 'ngx-mapbox-gl';
NgxMapboxGLModule.withConfig({ NgxMapboxGLModule.withConfig({
accessToken: 'pk.eyJ1IjoibW9uZGVpMSIsImEiOiJja2dsY2ZtaG0xZ2o5MnR0ZWs0Mm82OTBpIn0.NzDWN3P6jJLmci_v3MM1tA' accessToken: 'pk.eyJ1IjoibW9uZGVpMSIsImEiOiJja2dsY2ZtaG0xZ2o5MnR0ZWs0Mm82OTBpIn0.NzDWN3P6jJLmci_v3MM1tA'
}), }),
NbThemeModule.forRoot({ name: 'dark' }), ChartsModule
NbLayoutModule,
NbEvaIconsModule,
NbLayoutModule,
NbCardModule,
NbSidebarModule,
NbAuthModule.forRoot({
strategies: [
NbDummyAuthStrategy.setup({
name: 'email',
alwaysFail: false
})
],
forms: {}
})
], ],
providers: [], providers: [],
bootstrap: [AppComponent] bootstrap: [AppComponent]

View File

@@ -1,13 +1,12 @@
<mgl-map [style]="'mapbox://styles/mapbox/outdoors-v11'"> <div id="dashboard">
<mgl-geojson-source id="locHistory" [data]="data"></mgl-geojson-source> <h1>Dashboard</h1>
<mgl-layer <div class="chartjs-cointainer batteryChart">
id="locHisotryLines" <canvas baseChart
type="line" [chartType]="'line'"
source="locHistory" [datasets]="lineChartData"
[paint]="{ [labels]="lineChartLabels"
'line-color': '#ff0000', [options]="lineChartOptions"
'line-width': 4 [legend]="true"
}" ></canvas>
> </div>
</mgl-layer> </div>
</mgl-map>

View File

@@ -1,8 +1,9 @@
mgl-map { #dashboard {
position: absolute; margin-left: 3rem;
z-index: -1; margin-right: 3rem;
top: 0; }
left: 0;
height: 100vh; .batteryChart {
width: 100vw; max-height: 40rem;
max-width: 60rem;
} }

View File

@@ -1,6 +1,7 @@
import { AfterViewInit, Component, OnInit } from '@angular/core'; import { AfterViewInit, Component, OnInit } from '@angular/core';
import { Map } from 'mapbox-gl'; import { ChartDataSets, ChartOptions } from 'chart.js';
import { APIService } from '../api.service'; import { Label } from 'ng2-charts';
import { APIService, IBeat } from '../api.service';
@Component({ @Component({
selector: 'app-dashboard', selector: 'app-dashboard',
@@ -8,25 +9,68 @@ import { APIService } from '../api.service';
styleUrls: ['./dashboard.component.scss'] styleUrls: ['./dashboard.component.scss']
}) })
export class DashboardComponent implements AfterViewInit { export class DashboardComponent implements AfterViewInit {
map: Map;
data: GeoJSON.FeatureCollection<GeoJSON.LineString> = { // Array of different segments in chart
type: 'FeatureCollection', features: [ lineChartData: ChartDataSets[] = [
{ { data: [], label: 'Battery' }
type: 'Feature', ];
properties: null,
geometry: { type: 'LineString', coordinates: [] } // Labels shown on the x-axis
lineChartLabels: Label[] = [];
// Define chart options
lineChartOptions: ChartOptions = {
responsive: true,
scales: {
xAxes: [{
type: 'time',
stacked: false,
time: {
parser: 'MM/DD/YYYY HH:mm:ss',
round: 'minute',
tooltipFormat: 'll HH:mm'
},
scaleLabel: {
display: true,
labelString: 'Date'
}
}] }]
}
}; };
constructor(private api: APIService) { } constructor(private api: APIService) {
this.api.beatsEvent.subscribe(beats => {
this.lineChartData[0].data = [];
this.lineChartLabels = [];
async ngAfterViewInit(): Promise<void> { const batteryLevels: number[] = [];
const beats = await this.api.getBeats();
beats.forEach((beat) => { const finalBeats = beats.filter((val, i, array) => {
this.data.features[0].geometry.coordinates.push([beat.coordinate[1], beat.coordinate[0]]); if (batteryLevels.indexOf(val.battery) === -1) {
batteryLevels.push(val.battery);
return true;
} else {
return false;
}
}); });
console.log("Now:", this.data.features);
finalBeats.forEach((beat) => {
this.lineChartData[0].data.push(beat.battery);
this.lineChartLabels.push(this.formatDateTime(new Date(beat.createdAt)));
});
});
}
async fetchData(): Promise<void> {
await this.api.getBeats();
}
private formatDateTime(date: Date): string {
return `${date.getMonth()}/${date.getDay()}/${date.getFullYear()} ${date.getHours()}:${date.getMinutes()}:${date.getSeconds()}`;
}
ngAfterViewInit(): void {
this.fetchData();
} }
} }

View File

@@ -0,0 +1,11 @@
<div id="filter">
<h3>Filter</h3>
<select (change)="update($event.target.value)">
<option value="-1">Today</option>
<option value="3">Last 3h</option>
<option value="12">Last 12h</option>
<option value="24">Last 24h</option>
<option value="48">Last 48h</option>
<option value="0">All time</option>
</select>
</div>

View File

@@ -0,0 +1,30 @@
@import "../../styles.scss";
#filter {
position: fixed;
display: inline-flex;
bottom: 0;
width: 30vw;
height: 2.5rem;
min-width: 30rem;
max-width: 90vw;
align-items: center;
padding: 0.5rem;
background-color: $header-background;
backdrop-filter: blur(30px);
border-top-left-radius: 10px;
border-top-right-radius: 10px;
box-shadow: 10px 10px 50px 0px rgba(0, 0, 0, 0.9);
left: calc(50% - 30vw / 2);
}
h3 {
text-align: center;
height: fit-content;
padding-right: 1rem;
padding-left: 1rem;
}
select {
height: 60%;
}

View File

@@ -0,0 +1,25 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { FilterComponent } from './filter.component';
describe('FilterComponent', () => {
let component: FilterComponent;
let fixture: ComponentFixture<FilterComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ FilterComponent ]
})
.compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(FilterComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -0,0 +1,35 @@
import { Component, OnInit } from '@angular/core';
import { APIService, ITimespan } from '../api.service';
import * as moment from 'moment';
@Component({
selector: 'app-filter',
templateUrl: './filter.component.html',
styleUrls: ['./filter.component.scss']
})
export class FilterComponent implements OnInit {
constructor(private api: APIService) { }
ngOnInit(): void {
}
update(value: number) {
let result: ITimespan | undefined = { to: 0, from: 0 };
if (value == -1) {
result.from = moment().startOf('day').unix();
result.to = Math.floor(moment.now() / 1000);
} else if (value == 0) {
result = undefined;
} else {
result.from = moment().subtract(value, 'hours').unix();
result.to = Math.floor(moment.now() / 1000);
}
console.log(result);
this.api.time = result;
this.api.getBeats();
}
}

View File

@@ -22,7 +22,7 @@ export class LoginComponent implements OnInit {
if ((await this.api.login(this.username, this.password)).token !== undefined) { if ((await this.api.login(this.username, this.password)).token !== undefined) {
console.log('Login was successful!'); console.log('Login was successful!');
this.router.navigate(['dashboard']); this.router.navigate(['map']);
} else { } else {
console.log('Login was not successful!'); console.log('Login was not successful!');
} }

View File

@@ -0,0 +1,14 @@
<mgl-map [style]="'mapbox://styles/mapbox/outdoors-v11'">
<mgl-geojson-source id="locHistory" [data]="data"></mgl-geojson-source>
<mgl-layer
id="locHisotryLines"
type="line"
source="locHistory"
[zoom]="3"
[paint]="{
'line-color': '#ff0000',
'line-width': 4
}"
>
</mgl-layer>
</mgl-map>

View File

@@ -0,0 +1,8 @@
mgl-map {
position: absolute;
z-index: -1;
top: 0;
left: 0;
height: 100vh;
width: 100vw;
}

View File

@@ -0,0 +1,25 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { MapComponent } from './map.component';
describe('MapComponent', () => {
let component: MapComponent;
let fixture: ComponentFixture<MapComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ MapComponent ]
})
.compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(MapComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -0,0 +1,36 @@
import { AfterViewInit, Component, OnInit } from '@angular/core';
import { Map } from 'mapbox-gl';
import { APIService } from '../api.service';
@Component({
selector: 'app-map',
templateUrl: './map.component.html',
styleUrls: ['./map.component.scss']
})
export class MapComponent implements AfterViewInit {
map: Map;
data: GeoJSON.FeatureCollection<GeoJSON.LineString> = {
type: 'FeatureCollection', features: [
{
type: 'Feature',
properties: null,
geometry: { type: 'LineString', coordinates: [] }
}]
};
constructor(private api: APIService) {
this.api.beatsEvent.subscribe(beats => {
this.data.features[0].geometry.coordinates = [];
beats.forEach((beat) => {
this.data.features[0].geometry.coordinates.push([beat.coordinate[1], beat.coordinate[0]]);
this.data = {... this.data};
});
});
}
async ngAfterViewInit(): Promise<void> {
}
}

View File

@@ -0,0 +1,17 @@
<!-- By Sam Herbert (@sherb), for everyone. More @ http://goo.gl/7AJzbL -->
<svg width="38" height="38" viewBox="0 0 38 38" xmlns="http://www.w3.org/2000/svg" stroke="#fff">
<g fill="none" fill-rule="evenodd">
<g transform="translate(1 1)" stroke-width="2">
<circle stroke-opacity=".5" cx="18" cy="18" r="18"/>
<path d="M36 18c0-9.94-8.06-18-18-18">
<animateTransform
attributeName="transform"
type="rotate"
from="0 18 18"
to="360 18 18"
dur="1s"
repeatCount="indefinite"/>
</path>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 694 B

View File

@@ -1,17 +1,18 @@
@import 'themes'; @import 'themes';
@import '~@nebular/auth/styles/globals';
@import '~@nebular/theme/styles/globals';
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;700;900&display=swap'); @import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;700;900&display=swap');
@include nb-install() { /* Color palette */
@include nb-auth-global(); $primary-color: #1d1d1d;
@include nb-theme-global(); $secondary-color: #1c1c1c;
}; $foreground-color: #fff;
/* Misc */
$header-background: #1d1d1d9f;
body { body {
background-color: #1d1d1d; background-color: $primary-color;
color: #fff; color: $foreground-color;
font-family: 'Inter', sans-serif; font-family: 'Inter', sans-serif;
margin: 0; margin: 0;
padding: 0; padding: 0;

View File

@@ -1,18 +0,0 @@
@import '~@nebular/theme/styles/theming';
@import '~@nebular/theme/styles/themes/dark';
$nb-themes: nb-register-theme((
// add your variables here like:
// color-primary-100: #f2f6ff,
// color-primary-200: #d9e4ff,
// color-primary-300: #a6c1ff,
// color-primary-400: #598bff,
// color-primary-500: #3366ff,
// color-primary-600: #274bdb,
// color-primary-700: #1a34b8,
// color-primary-800: #102694,
// color-primary-900: #091c7a,
), dark, dark);