Frontend can now filter with timespan
This commit is contained in:
@@ -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);
|
||||||
|
|||||||
@@ -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 !== 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}*/
|
||||||
|
|
||||||
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);
|
res.status(200).send(beats);
|
||||||
}
|
} else {
|
||||||
|
res.status(404).send({ message: 'Phone not found' });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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
|
||||||
|
|||||||
14
backend/package-lock.json
generated
14
backend/package-lock.json
generated
@@ -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",
|
||||||
|
|||||||
@@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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": {
|
||||||
|
|||||||
77
frontend/package-lock.json
generated
77
frontend/package-lock.json
generated
@@ -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",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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[]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
</div>
|
<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>
|
||||||
|
<div class="header-spacer"></div>
|
||||||
|
<app-filter></app-filter>
|
||||||
<router-outlet></router-outlet>
|
<router-outlet></router-outlet>
|
||||||
|
<div id="loadingOverlay" [ngClass]="{show: this.showOverlay, gone: !this.showOverlay}">
|
||||||
<!-- Display start page -->
|
<div>
|
||||||
<div id="startpage" *ngIf="false">
|
<img src="assets/oval.svg">
|
||||||
<h1>Livebeat</h1>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -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;
|
||||||
}
|
}
|
||||||
@@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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]
|
||||||
|
|||||||
@@ -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>
|
|
||||||
@@ -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;
|
||||||
}
|
}
|
||||||
@@ -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();
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
11
frontend/src/app/filter/filter.component.html
Normal file
11
frontend/src/app/filter/filter.component.html
Normal 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>
|
||||||
30
frontend/src/app/filter/filter.component.scss
Normal file
30
frontend/src/app/filter/filter.component.scss
Normal 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%;
|
||||||
|
}
|
||||||
25
frontend/src/app/filter/filter.component.spec.ts
Normal file
25
frontend/src/app/filter/filter.component.spec.ts
Normal 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();
|
||||||
|
});
|
||||||
|
});
|
||||||
35
frontend/src/app/filter/filter.component.ts
Normal file
35
frontend/src/app/filter/filter.component.ts
Normal 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();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -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!');
|
||||||
}
|
}
|
||||||
|
|||||||
14
frontend/src/app/map/map.component.html
Normal file
14
frontend/src/app/map/map.component.html
Normal 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>
|
||||||
8
frontend/src/app/map/map.component.scss
Normal file
8
frontend/src/app/map/map.component.scss
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
mgl-map {
|
||||||
|
position: absolute;
|
||||||
|
z-index: -1;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
height: 100vh;
|
||||||
|
width: 100vw;
|
||||||
|
}
|
||||||
25
frontend/src/app/map/map.component.spec.ts
Normal file
25
frontend/src/app/map/map.component.spec.ts
Normal 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();
|
||||||
|
});
|
||||||
|
});
|
||||||
36
frontend/src/app/map/map.component.ts
Normal file
36
frontend/src/app/map/map.component.ts
Normal 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> {
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
17
frontend/src/assets/oval.svg
Normal file
17
frontend/src/assets/oval.svg
Normal 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 |
@@ -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;
|
||||||
|
|||||||
@@ -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);
|
|
||||||
|
|||||||
Reference in New Issue
Block a user