diff --git a/backend/app.ts b/backend/app.ts index 092efc3..4bbde4c 100644 --- a/backend/app.ts +++ b/backend/app.ts @@ -92,6 +92,7 @@ async function run() { /** * Database connection */ + mongoose.set('debug', true); const connection = await mongoose.connect(MONGO_URI, { useNewUrlParser: true, useUnifiedTopology: true }).catch((err) => { logger.crit("Database connection could not be made: ", err); exit(1); diff --git a/backend/endpoints/beat.ts b/backend/endpoints/beat.ts index e008ec3..9008d9b 100644 --- a/backend/endpoints/beat.ts +++ b/backend/endpoints/beat.ts @@ -1,30 +1,56 @@ import { Response } from "express"; -import { logger } from "../app"; import { LivebeatRequest } from "../lib/request"; +import { IBeat } from "../models/beat/beat.interface"; import { Beat } from "../models/beat/beat.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) { - 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. - if (filter === undefined) { - const phone = await Phone.findOne({ user: req.user?._id }); - logger.debug(`No filters were provided! Take ${phone?.displayName} as default.`); + const phone = req.query.phone === undefined ? await Phone.findOne({ user: req.user?._id }) : await Phone.findOne({ _id: phoneId, user: req.user?._id }); + let beats: IBeat[] = [] - if (phone !== undefined && phone !== null) { - logger.debug("Query for latest beats ..."); - const beats = await Beat.find({ phone: phone._id }).limit(800).sort({ _id: -1 }); - res.status(200).send(beats); + if (phone !== null) { + // If the battery preset is chosen, we only return documents where the battery status changed. + if (preset === 'battery') { + // 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' }); } } \ No newline at end of file diff --git a/backend/models/beat/beat.schema.ts b/backend/models/beat/beat.schema.ts index aaa54d6..66367cb 100644 --- a/backend/models/beat/beat.schema.ts +++ b/backend/models/beat/beat.schema.ts @@ -6,7 +6,8 @@ const schemaBeat = new Schema({ accuracy: { type: Number, required: false }, speed: { 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: { createdAt: true diff --git a/backend/package-lock.json b/backend/package-lock.json index 9b32fab..a37b9fb 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -158,6 +158,15 @@ "integrity": "sha512-Jus9s4CDbqwocc5pOAnh8ShfrnMcPHuJYzVcSUU7lrh8Ni5HuIqX3oilL86p3dlTrk0LzHRCgA/GQ7uNCw6l2Q==", "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": { "version": "3.5.28", "resolved": "https://registry.npmjs.org/@types/mongodb/-/mongodb-3.5.28.tgz", @@ -1728,6 +1737,11 @@ "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": { "version": "3.6.2", "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-3.6.2.tgz", diff --git a/backend/package.json b/backend/package.json index 13d1b58..9ea47ac 100644 --- a/backend/package.json +++ b/backend/package.json @@ -26,6 +26,7 @@ "express": "^4.17.1", "figlet": "^1.5.0", "jsonwebtoken": "^8.5.1", + "moment": "^2.29.1", "mongoose": "^5.10.9", "ts-node": "^9.0.0", "typescript": "^4.0.3", @@ -45,6 +46,7 @@ "nodemon": "^2.0.5", "@types/jsonwebtoken": "8.5.0", "@types/amqplib": "0.5.14", - "@types/cors": "2.8.8" + "@types/cors": "2.8.8", + "@types/moment": "2.13.0" } } diff --git a/frontend/angular.json b/frontend/angular.json index 021fc43..c1d4385 100644 --- a/frontend/angular.json +++ b/frontend/angular.json @@ -32,7 +32,9 @@ "./node_modules/mapbox-gl/dist/mapbox-gl.css", "./node_modules/@mapbox/mapbox-gl-geocoder/lib/mapbox-gl-geocoder.css" ], - "scripts": [] + "scripts": [ + "./node_modules/chart.js/dist/Chart.min.js" + ] }, "configurations": { "production": { diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 4e43dbf..94470cd 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -1798,24 +1798,6 @@ "resolved": "https://registry.npmjs.org/@mapbox/whoots-js/-/whoots-js-3.1.0.tgz", "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": { "version": "10.1.6", "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", "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": { "version": "7946.0.7", "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.7.tgz", @@ -3340,6 +3330,32 @@ "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==", "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": { "version": "3.4.2", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.4.2.tgz", @@ -6595,11 +6611,6 @@ "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": { "version": "3.1.0", "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", "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": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz", @@ -8336,6 +8352,11 @@ "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": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/move-concurrently/-/move-concurrently-1.0.1.tgz", @@ -8438,6 +8459,16 @@ "integrity": "sha1-yobR/ogoFpsBICCOPchCS524NCw=", "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": { "version": "4.8.1", "resolved": "https://registry.npmjs.org/ngx-mapbox-gl/-/ngx-mapbox-gl-4.8.1.tgz", diff --git a/frontend/package.json b/frontend/package.json index c868d01..fc8bc2d 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -20,13 +20,13 @@ "@angular/platform-browser": "~10.1.5", "@angular/platform-browser-dynamic": "~10.1.5", "@angular/router": "~10.1.5", - "@nebular/auth": "^6.2.1", - "@nebular/eva-icons": "^6.2.1", - "@nebular/theme": "^6.2.1", + "@types/chart.js": "^2.9.27", "@types/mapbox-gl": "^1.12.5", + "chart.js": "^2.9.4", "eva-icons": "^1.1.3", "geojson": "^0.5.0", "mapbox-gl": "^1.12.0", + "ng2-charts": "^2.4.2", "ngx-mapbox-gl": "^4.8.1", "rxjs": "~6.6.0", "tslib": "^2.0.0", diff --git a/frontend/src/app/api.service.ts b/frontend/src/app/api.service.ts index 12f159f..0f94211 100644 --- a/frontend/src/app/api.service.ts +++ b/frontend/src/app/api.service.ts @@ -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 { BehaviorSubject } from 'rxjs'; export interface ILogin { token: string; @@ -14,6 +15,11 @@ export interface IBeat { createdAt?: Date; } +export interface ITimespan { + from?: number; + to?: number; +} + @Injectable({ providedIn: 'root' }) @@ -22,8 +28,17 @@ export class APIService { private token: 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 = new BehaviorSubject([]); + loginEvent: BehaviorSubject = new BehaviorSubject(false); + fetchingDataEvent: BehaviorSubject = new BehaviorSubject(false); + + API_ENDPOINT = 'http://192.168.178.26:8040'; constructor(private httpClient: HttpClient) { } @@ -37,20 +52,34 @@ export class APIService { this.token = (token as ILogin).token; this.username = username; + this.loginEvent.next(true); resolve(token as ILogin); }); }); } - async getBeats(): Promise { + async getBeats(preset?: 'battery'): Promise { return new Promise((resolve, 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 => { - console.log(beats); + this.beats = beats as IBeat[]; + this.beatsEvent.next(beats as IBeat[]); + this.fetchingDataEvent.next(false); resolve(beats as IBeat[]); }); }); diff --git a/frontend/src/app/app-routing.module.ts b/frontend/src/app/app-routing.module.ts index f10b5d3..afe158a 100644 --- a/frontend/src/app/app-routing.module.ts +++ b/frontend/src/app/app-routing.module.ts @@ -1,9 +1,9 @@ import { NgModule } from '@angular/core'; import { Routes, RouterModule } from '@angular/router'; -import { NbAuthComponent, NbLoginComponent } from '@nebular/auth'; import { AppComponent } from './app.component'; import { DashboardComponent } from './dashboard/dashboard.component'; import { LoginComponent } from './login/login.component'; +import { MapComponent } from './map/map.component'; const routes: Routes = [ { @@ -17,6 +17,10 @@ const routes: Routes = [ { path: 'dashboard', component: DashboardComponent + }, + { + path: 'map', + component: MapComponent } ]; diff --git a/frontend/src/app/app.component.html b/frontend/src/app/app.component.html index 91bccf1..b947b47 100644 --- a/frontend/src/app/app.component.html +++ b/frontend/src/app/app.component.html @@ -1,12 +1,16 @@ +
+ - - -
-

Livebeat

+
+
+ +
\ No newline at end of file diff --git a/frontend/src/app/app.component.scss b/frontend/src/app/app.component.scss index c0c1cd6..a7474ad 100644 --- a/frontend/src/app/app.component.scss +++ b/frontend/src/app/app.component.scss @@ -1,10 +1,76 @@ + #header { + position: fixed; + top: 0; + left: 0; width: 100vw; height: fit-content; - padding-top: 0.4rem; - padding-bottom: 0.4rem; + padding-top: 0.8rem; + padding-bottom: 0.8rem; background-color: #1d1d1dd9; - backdrop-filter: blur(20px); + backdrop-filter: blur(30px); 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; } \ No newline at end of file diff --git a/frontend/src/app/app.component.ts b/frontend/src/app/app.component.ts index 2f4dcfc..689c4f9 100644 --- a/frontend/src/app/app.component.ts +++ b/frontend/src/app/app.component.ts @@ -7,9 +7,19 @@ import { APIService } from './api.service'; templateUrl: './app.component.html', styleUrls: ['./app.component.scss'], }) -export class AppComponent { +export class AppComponent implements OnInit{ title = 'Livebeat'; + showOverlay = false; + constructor(public api: APIService, private router: Router) { + this.api.fetchingDataEvent.subscribe(status => { + this.showOverlay = status; + }); + } + + async ngOnInit(): Promise { + await this.api.login('admin', '$1KDaNCDlyXAOg'); + return; } } diff --git a/frontend/src/app/app.module.ts b/frontend/src/app/app.module.ts index 9182aa0..e770634 100644 --- a/frontend/src/app/app.module.ts +++ b/frontend/src/app/app.module.ts @@ -1,23 +1,25 @@ import { HttpClientModule } from '@angular/common/http'; import { NgModule } from '@angular/core'; +import { FormsModule } from '@angular/forms'; import { BrowserModule } from '@angular/platform-browser'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; -import { NbEvaIconsModule } from '@nebular/eva-icons'; -import { NbPasswordAuthStrategy, NbAuthModule, NbDummyAuthStrategy } from '@nebular/auth'; -import { NbCardModule, NbLayoutModule, NbSidebarModule, NbThemeModule } from '@nebular/theme'; +import { ChartsModule } from 'ng2-charts'; +import { NgxMapboxGLModule } from 'ngx-mapbox-gl'; import { AppRoutingModule } from './app-routing.module'; import { AppComponent } from './app.component'; -import { LoginComponent } from './login/login.component'; -import { FormsModule } from '@angular/forms'; 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({ declarations: [ AppComponent, LoginComponent, - DashboardComponent + DashboardComponent, + MapComponent, + FilterComponent ], imports: [ BrowserModule, @@ -28,21 +30,7 @@ import { NgxMapboxGLModule } from 'ngx-mapbox-gl'; NgxMapboxGLModule.withConfig({ accessToken: 'pk.eyJ1IjoibW9uZGVpMSIsImEiOiJja2dsY2ZtaG0xZ2o5MnR0ZWs0Mm82OTBpIn0.NzDWN3P6jJLmci_v3MM1tA' }), - NbThemeModule.forRoot({ name: 'dark' }), - NbLayoutModule, - NbEvaIconsModule, - NbLayoutModule, - NbCardModule, - NbSidebarModule, - NbAuthModule.forRoot({ - strategies: [ - NbDummyAuthStrategy.setup({ - name: 'email', - alwaysFail: false - }) - ], - forms: {} - }) + ChartsModule ], providers: [], bootstrap: [AppComponent] diff --git a/frontend/src/app/dashboard/dashboard.component.html b/frontend/src/app/dashboard/dashboard.component.html index 8ca9389..8529d96 100644 --- a/frontend/src/app/dashboard/dashboard.component.html +++ b/frontend/src/app/dashboard/dashboard.component.html @@ -1,13 +1,12 @@ - - - - - \ No newline at end of file +
+

Dashboard

+
+ +
+
\ No newline at end of file diff --git a/frontend/src/app/dashboard/dashboard.component.scss b/frontend/src/app/dashboard/dashboard.component.scss index db6fcba..7d3fffa 100644 --- a/frontend/src/app/dashboard/dashboard.component.scss +++ b/frontend/src/app/dashboard/dashboard.component.scss @@ -1,8 +1,9 @@ -mgl-map { - position: absolute; - z-index: -1; - top: 0; - left: 0; - height: 100vh; - width: 100vw; +#dashboard { + margin-left: 3rem; + margin-right: 3rem; +} + +.batteryChart { + max-height: 40rem; + max-width: 60rem; } \ No newline at end of file diff --git a/frontend/src/app/dashboard/dashboard.component.ts b/frontend/src/app/dashboard/dashboard.component.ts index 62241d0..5c755dd 100644 --- a/frontend/src/app/dashboard/dashboard.component.ts +++ b/frontend/src/app/dashboard/dashboard.component.ts @@ -1,6 +1,7 @@ import { AfterViewInit, Component, OnInit } from '@angular/core'; -import { Map } from 'mapbox-gl'; -import { APIService } from '../api.service'; +import { ChartDataSets, ChartOptions } from 'chart.js'; +import { Label } from 'ng2-charts'; +import { APIService, IBeat } from '../api.service'; @Component({ selector: 'app-dashboard', @@ -8,25 +9,68 @@ import { APIService } from '../api.service'; styleUrls: ['./dashboard.component.scss'] }) export class DashboardComponent implements AfterViewInit { - map: Map; - data: GeoJSON.FeatureCollection = { - type: 'FeatureCollection', features: [ - { - type: 'Feature', - properties: null, - geometry: { type: 'LineString', coordinates: [] } - }] + // Array of different segments in chart + lineChartData: ChartDataSets[] = [ + { data: [], label: 'Battery' } + ]; + + // 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 { - const beats = await this.api.getBeats(); - beats.forEach((beat) => { - this.data.features[0].geometry.coordinates.push([beat.coordinate[1], beat.coordinate[0]]); + const batteryLevels: number[] = []; + + const finalBeats = beats.filter((val, i, array) => { + if (batteryLevels.indexOf(val.battery) === -1) { + batteryLevels.push(val.battery); + return true; + } else { + return false; + } + }); + + finalBeats.forEach((beat) => { + this.lineChartData[0].data.push(beat.battery); + this.lineChartLabels.push(this.formatDateTime(new Date(beat.createdAt))); + }); }); - console.log("Now:", this.data.features); + } + + async fetchData(): Promise { + 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(); } } diff --git a/frontend/src/app/filter/filter.component.html b/frontend/src/app/filter/filter.component.html new file mode 100644 index 0000000..ed0f948 --- /dev/null +++ b/frontend/src/app/filter/filter.component.html @@ -0,0 +1,11 @@ +
+

Filter

+ +
\ No newline at end of file diff --git a/frontend/src/app/filter/filter.component.scss b/frontend/src/app/filter/filter.component.scss new file mode 100644 index 0000000..bda9b22 --- /dev/null +++ b/frontend/src/app/filter/filter.component.scss @@ -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%; +} \ No newline at end of file diff --git a/frontend/src/app/filter/filter.component.spec.ts b/frontend/src/app/filter/filter.component.spec.ts new file mode 100644 index 0000000..f43cc0d --- /dev/null +++ b/frontend/src/app/filter/filter.component.spec.ts @@ -0,0 +1,25 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { FilterComponent } from './filter.component'; + +describe('FilterComponent', () => { + let component: FilterComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ FilterComponent ] + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(FilterComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/frontend/src/app/filter/filter.component.ts b/frontend/src/app/filter/filter.component.ts new file mode 100644 index 0000000..5ac339f --- /dev/null +++ b/frontend/src/app/filter/filter.component.ts @@ -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(); + } + +} diff --git a/frontend/src/app/login/login.component.ts b/frontend/src/app/login/login.component.ts index c1b3d6e..1e86f79 100644 --- a/frontend/src/app/login/login.component.ts +++ b/frontend/src/app/login/login.component.ts @@ -22,7 +22,7 @@ export class LoginComponent implements OnInit { if ((await this.api.login(this.username, this.password)).token !== undefined) { console.log('Login was successful!'); - this.router.navigate(['dashboard']); + this.router.navigate(['map']); } else { console.log('Login was not successful!'); } diff --git a/frontend/src/app/map/map.component.html b/frontend/src/app/map/map.component.html new file mode 100644 index 0000000..e90ef14 --- /dev/null +++ b/frontend/src/app/map/map.component.html @@ -0,0 +1,14 @@ + + + + + \ No newline at end of file diff --git a/frontend/src/app/map/map.component.scss b/frontend/src/app/map/map.component.scss new file mode 100644 index 0000000..db6fcba --- /dev/null +++ b/frontend/src/app/map/map.component.scss @@ -0,0 +1,8 @@ +mgl-map { + position: absolute; + z-index: -1; + top: 0; + left: 0; + height: 100vh; + width: 100vw; +} \ No newline at end of file diff --git a/frontend/src/app/map/map.component.spec.ts b/frontend/src/app/map/map.component.spec.ts new file mode 100644 index 0000000..f163147 --- /dev/null +++ b/frontend/src/app/map/map.component.spec.ts @@ -0,0 +1,25 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { MapComponent } from './map.component'; + +describe('MapComponent', () => { + let component: MapComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ MapComponent ] + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(MapComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/frontend/src/app/map/map.component.ts b/frontend/src/app/map/map.component.ts new file mode 100644 index 0000000..09f4aa1 --- /dev/null +++ b/frontend/src/app/map/map.component.ts @@ -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 = { + 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 { + } + +} diff --git a/frontend/src/assets/oval.svg b/frontend/src/assets/oval.svg new file mode 100644 index 0000000..004a356 --- /dev/null +++ b/frontend/src/assets/oval.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/frontend/src/styles.scss b/frontend/src/styles.scss index e5c8924..7b41e0e 100644 --- a/frontend/src/styles.scss +++ b/frontend/src/styles.scss @@ -1,17 +1,18 @@ @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'); -@include nb-install() { - @include nb-auth-global(); - @include nb-theme-global(); -}; +/* Color palette */ +$primary-color: #1d1d1d; +$secondary-color: #1c1c1c; +$foreground-color: #fff; + +/* Misc */ +$header-background: #1d1d1d9f; body { - background-color: #1d1d1d; - color: #fff; + background-color: $primary-color; + color: $foreground-color; font-family: 'Inter', sans-serif; margin: 0; padding: 0; diff --git a/frontend/src/themes.scss b/frontend/src/themes.scss index e0ea669..e69de29 100644 --- a/frontend/src/themes.scss +++ b/frontend/src/themes.scss @@ -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);