Real-time communication with frontend

- Frontend shows heatmap of most visit places
- Maximum accuracy can now be set
- Fix bug where battery chart filtered values wrongly
This commit is contained in:
2020-10-26 23:38:34 +01:00
parent fa60f58d3c
commit e12ed7775b
20 changed files with 770 additions and 161 deletions

View File

@@ -38,6 +38,7 @@ class MainActivity : AppCompatActivity() {
companion object {
@JvmStatic val API_URL = "http://192.168.178.26:8040"
var TOKEN = ""
var USER: User? = null
}
private var broadcastReceiver: BroadcastReceiver? = null
@@ -50,6 +51,7 @@ class MainActivity : AppCompatActivity() {
val client = OkHttpClient()
val req = Request.Builder()
.url("$API_URL/phone/$androidId")
.header("token", TOKEN)
.get()
.build()
val response = client.newCall(req).execute()
@@ -65,7 +67,7 @@ class MainActivity : AppCompatActivity() {
androidId,
Build.MODEL,
Build.PRODUCT,
Build.VERSION.RELEASE,
Build.VERSION.BASE_OS + " " + Build.VERSION.RELEASE + " " + Build.VERSION.CODENAME,
System.getProperty("os.arch")
)
val moshi = Moshi.Builder().add(KotlinJsonAdapterFactory()).build()
@@ -91,7 +93,6 @@ class MainActivity : AppCompatActivity() {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
setSupportActionBar(findViewById(R.id.toolbar))
startService(Intent(this, TrackerService::class.java))
// Check authorization
val backendChecks = Thread(Runnable {
@@ -119,14 +120,34 @@ class MainActivity : AppCompatActivity() {
this.runOnUiThread(Runnable {
findViewById<TextView>(R.id.httpStatus).setTextColor(Color.GREEN)
findViewById<TextView>(R.id.httpStatus).text = "CONNECTED"
findViewById<Button>(R.id.signin).isEnabled = false
findViewById<Button>(R.id.signin).text = "Logged in"
})
Snackbar.make(findViewById<FloatingActionButton>(R.id.fab), "Login succeeded", Snackbar.LENGTH_SHORT)
.setBackgroundTint(Color.GREEN)
.setActionTextColor(Color.WHITE)
.show()
// Get all user information
val userinfo = Request.Builder()
.url("$API_URL/user")
.get()
.header("Content-Type", "application/json")
.header("token", TOKEN)
.build()
val userinfoResponse = client.newCall(userinfo).execute()
checkIfPhoneIsRegistered()
if (userinfoResponse.code == 200) {
val jsonToUser = moshi.adapter(User::class.java)
val userInfoResponseBody = userinfoResponse.body!!.string()
USER = jsonToUser.fromJson(userInfoResponseBody)
// Only start service if authentication went good.
startService(Intent(this, TrackerService::class.java))
Snackbar.make(findViewById<FloatingActionButton>(R.id.fab), "Login succeeded", Snackbar.LENGTH_SHORT)
.setBackgroundTint(Color.GREEN)
.setActionTextColor(Color.WHITE)
.show()
checkIfPhoneIsRegistered()
}
} else {
// Since we are in another thread, we have to get back to the UI thread.
this.runOnUiThread(Runnable {
@@ -158,7 +179,7 @@ class MainActivity : AppCompatActivity() {
findViewById<TextView>(R.id.rabbitStatus).setTextColor(Color.RED)
}
if (statusHttp == 200) {
/*if (statusHttp == 200) {
findViewById<TextView>(R.id.httpStatus).text = "ONLINE (no login)"
findViewById<TextView>(R.id.httpStatus).setTextColor(Color.CYAN)
findViewById<Button>(R.id.signin).isEnabled = false
@@ -168,7 +189,7 @@ class MainActivity : AppCompatActivity() {
findViewById<TextView>(R.id.httpStatus).setTextColor(Color.RED)
findViewById<Button>(R.id.signin).isEnabled = true
findViewById<Button>(R.id.signin).text = "Sign in"
}
}*/
}
}
registerReceiver(broadcastReceiver, IntentFilter("de.nicolasklier.livebeat"))
@@ -192,6 +213,12 @@ class MainActivity : AppCompatActivity() {
}
}
override fun onStart() {
super.onStart()
registerReceiver(broadcastReceiver, IntentFilter("de.nicolasklier.livebeat"))
}
override fun onStop() {
super.onStop()

View File

@@ -2,25 +2,35 @@ package de.nicolasklier.livebeat
import com.squareup.moshi.JsonClass
import com.squareup.moshi.ToJson
import java.util.*
/**
* This class represents one singe beat that will be send to the database.
*/
class Beat(
final val token: String, // Token of this phone
final val gpsLocation: Array<Double>,
final val battery: Int?,
final val timestamp: Long
val token: String, // Token of this phone
val gpsLocation: Array<Double>,
val battery: Int?,
val timestamp: Long
) {}
class Phone(
final val androidId: String,
final val modelName: String,
final val displayName: String,
final val operatingSystem: String,
final val architecture: String
val androidId: String,
val modelName: String,
val displayName: String,
val operatingSystem: String,
val architecture: String
) {}
class User(
val name: String,
val type: String,
val lastLogin: String,
val twoFASecret: String?,
val brokerToken: String,
val createdAt: String
)
class Login(
final val token: String
val token: String
) {}

View File

@@ -40,16 +40,12 @@ class TrackerService : Service() {
// This thread only connects to RabbitMQ
val connectionThread = Thread(Runnable {
val client = OkHttpClient()
val req = Request.Builder()
.url(MainActivity.API_URL)
.get()
.build()
val factory = ConnectionFactory()
factory.username = "lineage"
factory.password = "ZSo\$X97GQ547JXL7nGq"
factory.username = MainActivity.USER!!.name
factory.password = MainActivity.USER!!.brokerToken
factory.virtualHost = "/"
factory.host = "nk-home.ddns.net"
factory.host = "192.168.178.26"
factory.port = 5672
factory.isAutomaticRecoveryEnabled = true
try {
@@ -59,11 +55,10 @@ class TrackerService : Service() {
val intent = Intent("de.nicolasklier.livebeat")
val bundle = Bundle()
bundle.putBoolean("statusRabbit", true)
bundle.putInt("statusHttp", client.newCall(req).execute().code)
intent.putExtras(bundle)
this.sendBroadcast(intent)
channel[0]?.queueDeclare("tracker", true, false, false, null)
channel[0]?.queueDeclare("tracker-" + factory.username, true, false, false, null)
//channel[0]?.basicPublish("", "Tracker", null, "Test message".toByteArray())
Log.i("RabbitMQ", "run: Published test message")
} catch (e: IOException) {
@@ -83,7 +78,7 @@ class TrackerService : Service() {
val androidId = Settings.Secure.getString(contentResolver, Settings.Secure.ANDROID_ID)
if (ActivityCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED) {
locationManager.requestLocationUpdates("gps", 5000, 0f
locationManager.requestLocationUpdates("gps", 10000, 0f
) { location ->
Log.i("Location", "Location is: " + location.latitude + " | " + location.longitude)

View File

@@ -1,21 +1,21 @@
import bodyParser = require('body-parser');
import { bold } from 'chalk';
import * as cors from 'cors';
import { config as dconfig } from 'dotenv';
import * as express from 'express';
import * as figlet from 'figlet';
import * as mongoose from 'mongoose';
import * as cors from 'cors';
import { exit } from 'process';
import * as winston from 'winston';
import { RabbitMQ } from './lib/rabbit';
import { config } from './config';
import { DeleteUser, GetUser, LoginUser, MW_User, PatchUser } from './endpoints/user';
import { GetBeat, GetBeatStats } from './endpoints/beat';
import { GetPhone, PostPhone } from './endpoints/phone';
import { DeleteUser, GetUser, LoginRabbitUser, LoginUser, MW_User, PatchUser, Resource, Topic, VHost } from './endpoints/user';
import { hashPassword, randomPepper, randomString } from './lib/crypto';
import { RabbitMQ } from './lib/rabbit';
import { UserType } from './models/user/user.interface';
import { User } from './models/user/user.model';
import { GetPhone, PostPhone } from './endpoints/phone';
import { GetBeat, GetBeatStats } from './endpoints/beat';
// Load .env
dconfig({ debug: true, encoding: 'UTF-8' });
@@ -107,6 +107,7 @@ async function run() {
await User.create({
name: 'admin',
password: await hashPassword(randomPassword + salt + randomPepper()),
brokerToken: randomString(16),
salt,
createdAt: Date.now(),
lastLogin: 0,
@@ -119,13 +120,6 @@ async function run() {
logger.debug("At least one admin user already exists, skip.");
}
/**
* Message broker
*/
rabbitmq = new RabbitMQ();
await rabbitmq.init();
logger.info("Connected with message broker.");
/**
* HTTP server
*/
@@ -143,14 +137,22 @@ async function run() {
});
app.get('/', (req, res) => res.status(200).send('OK'));
// User authentication
app.post('/user/login', (req, res) => LoginUser(req, res));
app.get('/user/rabbitlogin', (req, res) => LoginRabbitUser(req, res));
app.get('/user/vhost', (req, res) => VHost(req, res));
app.get('/user/resource', (req, res) => Resource(req, res));
app.get('/user/topic', (req, res) => Topic(req, res));
// Basic user actions
app.get('/user/', MW_User, (req, res) => GetUser(req, res));
app.get('/user/:id', MW_User, (req, res) => GetUser(req, res));
app.patch('/user/:id', MW_User, (req, res) => PatchUser(req, res));
app.delete('/user/:id', MW_User, (req, res) => DeleteUser(req, res));
app.post('/user/login', (req, res) => LoginUser(req, res));
app.get('/phone', MW_User, (req, res) => GetPhone(req, res));
app.get('/phone/:id', MW_User, (req, res) => GetPhone(req, res));
app.get('/phone', MW_User, (req, res) => GetPhone(req, res));
app.post('/phone', MW_User, (req, res) => PostPhone(req, res));
app.get('/beat/', MW_User, (req, res) => GetBeat(req, res));
@@ -159,6 +161,13 @@ async function run() {
app.listen(config.http.port, config.http.host, () => {
logger.info(`HTTP server is running at ${config.http.host}:${config.http.port}`);
});
/**
* Message broker
*/
rabbitmq = new RabbitMQ();
await rabbitmq.init();
logger.info("Connected with message broker.");
}
run();

View File

@@ -35,7 +35,7 @@ export async function GetBeat(req: LivebeatRequest, res: Response) {
$gte: new Date((from | 0) * 1000),
$lte: new Date((to | Date.now() /1000) * 1000)
}
}).sort({ _id: -1 });
}).sort({ _id: 1 });
res.status(200).send(beats);
} else {
res.status(404).send({ message: 'Phone not found' });

View File

@@ -2,10 +2,11 @@ import { Request, Response } from "express";
import { verifyPassword } from "../lib/crypto";
import { User } from "../models/user/user.model";
import { sign, decode, verify } from 'jsonwebtoken';
import { JWT_SECRET, logger } from "../app";
import { JWT_SECRET, logger, RABBITMQ_URI } from "../app";
import { LivebeatRequest } from '../lib/request';
import { SchemaTypes } from "mongoose";
import { Phone } from "../models/phone/phone.model";
import { UserType } from "../models/user/user.interface";
export async function GetUser(req: LivebeatRequest, res: Response) {
let user: any = req.user;
@@ -64,6 +65,130 @@ export async function LoginUser(req: Request, res: Response) {
res.status(200).send({ token });
}
/**
* This function handles all logins to RabbitMQ since they need a differnt type of response
* then requests from frontends (web and phone).
*/
export async function LoginRabbitUser(req: Request, res: Response) {
const username = req.query.username;
const password = req.query.password;
if (username === undefined || password === undefined) {
res.status(200).send('deny');
return;
}
// Check if request comes from backend. Basicly, we permitting ourself to connect with RabbitMQ.
if (username === "backend" && password === RABBITMQ_URI.split(':')[2].split('@')[0]) {
res.status(200).send('allow administrator');
return;
}
// Get user from database
const user = await User.findOne({ name: username.toString() });
// If we are here, it means we have a non-admin user.
if (user === null) {
res.status(200).send('deny');
return;
}
// Auth token for message broker is stored in plain text since it's randomly generated and only grants access to the broker.
if (user.brokerToken === password.toString()) {
if (user.type === UserType.ADMIN) {
res.status(200).send('allow administrator');
} else {
// Not an admin, grant user privilieges
res.status(200).send('allow user')
}
return;
}
res.status(200).send('deny');
}
/**
* This function basicly allows access to the root vhost if the user is known.
*/
export async function VHost(req: Request, res: Response) {
const vhost = req.query.vhost;
const username = req.query.username;
if (vhost === undefined || username === undefined) {
res.status(200).send('deny');
return;
}
if (vhost != '/') {
res.status(200).send('deny');
return;
}
// Check if user is us
if (username === 'backend') {
res.status(200).send('allow');
return;
}
const user = await User.findOne({ name: username.toString() });
if (user === null) {
// Deny if user doesn't exist.
res.status(200).send('deny');
} else {
res.status(200).send('allow');
}
}
export async function Resource(req: Request, res: Response) {
const username = req.query.username;
const vhost = req.query.vhost;
const resource = req.query.resource;
const name = req.query.name;
const permission = req.query.permission;
const tags = req.query.tags;
if (username === undefined || vhost === undefined || resource === undefined || name === undefined || permission === undefined || tags === undefined) {
res.status(200).send('deny');
return;
}
// Check if it's us
if (username.toString() == 'backend') {
res.status(200).send('allow');
return;
}
// Deny if not root vhost
if (vhost.toString() != '/') {
res.status(200).send('deny');
return;
}
// Check if user exists
const user = await User.findOne({ name: username.toString() });
if (user == null) {
res.status(200).send('deny');
return;
}
if (tags.toString() == "administrator" && user.type != UserType.ADMIN) {
res.status(200).send('deny');
return;
}
// TODO: This has to change if we want to allow users to see the realtime movement of others.
if (resource.toString().startsWith('tracker-') && resource != 'tracker-' + username) {
res.status(200).send('deny');
return;
}
res.status(200).send('allow');
}
export async function Topic(req: Request, res: Response) {
res.status(200).send('allow');
}
/**
* This middleware validates any tokens that are required to access most of the endpoints.
* Note: This validation doesn't contain any permission checking.

View File

@@ -30,9 +30,9 @@ export class RabbitMQ {
return;
}
logger.info(`New beat from ${phone.displayName} with ${msg.gpsLocation[2]} accuracy and ${msg.battery}% battery`)
logger.info(`New beat from ${phone.displayName} with ${msg.gpsLocation[2]} accuracy and ${msg.battery}% battery`);
Beat.create({
const newBeat = await Beat.create({
phone: phone._id,
coordinate: [msg.gpsLocation[0], msg.gpsLocation[1]],
accuracy: msg.gpsLocation[2],
@@ -40,6 +40,8 @@ export class RabbitMQ {
battery: msg.battery,
createdAt: msg.timestamp
});
this.channel!.publish('amq.topic', '.', Buffer.from(JSON.stringify(newBeat.toJSON())));
}, { noAck: true });
}

View File

@@ -10,7 +10,8 @@ const schemaBeat = new Schema({
createdAt: { type: SchemaTypes.Date, required: false }
}, {
timestamps: {
createdAt: true
createdAt: true,
updatedAt: false
},
versionKey: false
});

View File

@@ -13,5 +13,6 @@ export interface IUser extends Document {
type: UserType,
lastLogin: Date,
twoFASecret?: string,
brokerToken: string,
createdAt?: Date
}

View File

@@ -6,6 +6,7 @@ const schemaUser = new Schema({
salt: { type: String, required: true },
type: { type: String, required: true, default: 'user' }, // This could be user, admin, guest
twoFASecret: { type: String, required: false },
brokerToken: { type: String, required: true },
lastLogin: { type: Date, required: true, default: Date.now },
}, {
timestamps: {

View File

@@ -2718,8 +2718,7 @@
"balanced-match": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz",
"integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=",
"dev": true
"integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c="
},
"base": {
"version": "0.11.2",
@@ -2790,8 +2789,7 @@
"base64-js": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.3.1.tgz",
"integrity": "sha512-mLQ4i2QO1ytvGWFWmcngKO//JXAQueZvwEKtjgQFM4jIK0kU+ytMfplL8j+n5mspOfjHwoAg+9yhb7BwAHm36g==",
"dev": true
"integrity": "sha512-mLQ4i2QO1ytvGWFWmcngKO//JXAQueZvwEKtjgQFM4jIK0kU+ytMfplL8j+n5mspOfjHwoAg+9yhb7BwAHm36g=="
},
"base64id": {
"version": "2.0.0",
@@ -2835,6 +2833,37 @@
"integrity": "sha512-1Yj8h9Q+QDF5FzhMs/c9+6UntbD5MkRfRwac8DoEm9ZfUBZ7tZ55YcGVAzEe4bXsdQHEk+s9S5wsOKVdZrw0tQ==",
"dev": true
},
"bl": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/bl/-/bl-4.0.3.tgz",
"integrity": "sha512-fs4G6/Hu4/EE+F75J8DuN/0IpQqNjAdC7aEQv7Qt8MHGUH7Ckv2MwTEEeN9QehD0pfIDkMI1bkHYkKy7xHyKIg==",
"requires": {
"buffer": "^5.5.0",
"inherits": "^2.0.4",
"readable-stream": "^3.4.0"
},
"dependencies": {
"buffer": {
"version": "5.6.1",
"resolved": "https://registry.npmjs.org/buffer/-/buffer-5.6.1.tgz",
"integrity": "sha512-2z15UUHpS9/3tk9mY/q+Rl3rydOi7yMp5XWNQnRvoz+mJwiv8brqYwp9a+nOCtma6dwuEIxljD8W3ysVBZ05Vg==",
"requires": {
"base64-js": "^1.3.1",
"ieee754": "^1.1.13"
}
},
"readable-stream": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz",
"integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==",
"requires": {
"inherits": "^2.0.3",
"string_decoder": "^1.1.1",
"util-deprecate": "^1.0.1"
}
}
}
},
"blob": {
"version": "0.0.5",
"resolved": "https://registry.npmjs.org/blob/-/blob-0.0.5.tgz",
@@ -2936,7 +2965,6 @@
"version": "1.1.11",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
"dev": true,
"requires": {
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
@@ -3239,6 +3267,15 @@
}
}
},
"callback-stream": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/callback-stream/-/callback-stream-1.1.0.tgz",
"integrity": "sha1-RwGlEmbwbgbqpx/BcjOCLYdfSQg=",
"requires": {
"inherits": "^2.0.1",
"readable-stream": "> 1.0.0 < 3.0.0"
}
},
"caller-callsite": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/caller-callsite/-/caller-callsite-2.0.0.tgz",
@@ -3646,6 +3683,22 @@
"integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==",
"dev": true
},
"commist": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/commist/-/commist-1.1.0.tgz",
"integrity": "sha512-rraC8NXWOEjhADbZe9QBNzLAN5Q3fsTPQtBV+fEVj6xKIgDgNiEVE6ZNfHpZOqfQ21YUzfVNUXLOEZquYvQPPg==",
"requires": {
"leven": "^2.1.0",
"minimist": "^1.1.0"
},
"dependencies": {
"leven": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/leven/-/leven-2.1.0.tgz",
"integrity": "sha1-wuep93IJTe6dNCAq6KzORoeHVYA="
}
}
},
"commondir": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz",
@@ -3723,14 +3776,12 @@
"concat-map": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
"integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=",
"dev": true
"integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s="
},
"concat-stream": {
"version": "1.6.2",
"resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz",
"integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==",
"dev": true,
"requires": {
"buffer-from": "^1.0.0",
"inherits": "^2.0.3",
@@ -4271,7 +4322,6 @@
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/d/-/d-1.0.1.tgz",
"integrity": "sha512-m62ShEObQ39CfralilEQRjH6oAMtNCV1xJyEx5LpRYUVN+EviphDgUc/F3hnYbADmkiNs67Y+3ylmlG7Lnu+FA==",
"dev": true,
"requires": {
"es5-ext": "^0.10.50",
"type": "^1.0.1"
@@ -4313,7 +4363,6 @@
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.2.0.tgz",
"integrity": "sha512-IX2ncY78vDTjZMFUdmsvIRFY2Cf4FnD0wRs+nQwJU8Lu99/tPFdb0VybiiMTPe3I6rQmwsqQqRBvxU+bZ/I8sg==",
"dev": true,
"requires": {
"ms": "2.1.2"
}
@@ -4696,7 +4745,6 @@
"version": "3.7.1",
"resolved": "https://registry.npmjs.org/duplexify/-/duplexify-3.7.1.tgz",
"integrity": "sha512-07z8uv2wMyS51kKhD1KsdXJg5WQ6t93RneqRxUHnskXVtlYYkLqM0gqStQZ3pj073g687jPCHrqNfCzawLYh5g==",
"dev": true,
"requires": {
"end-of-stream": "^1.0.0",
"inherits": "^2.0.1",
@@ -4785,7 +4833,6 @@
"version": "1.4.4",
"resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz",
"integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==",
"dev": true,
"requires": {
"once": "^1.4.0"
}
@@ -4978,7 +5025,6 @@
"version": "0.10.53",
"resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.53.tgz",
"integrity": "sha512-Xs2Stw6NiNHWypzRTY1MtaG/uJlwCk8kH81920ma8mvN8Xq1gsfhZvpkImLQArw8AHnv8MT2I45J3c0R8slE+Q==",
"dev": true,
"requires": {
"es6-iterator": "~2.0.3",
"es6-symbol": "~3.1.3",
@@ -4989,13 +5035,25 @@
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/es6-iterator/-/es6-iterator-2.0.3.tgz",
"integrity": "sha1-p96IkUGgWpSwhUQDstCg+/qY87c=",
"dev": true,
"requires": {
"d": "1",
"es5-ext": "^0.10.35",
"es6-symbol": "^3.1.1"
}
},
"es6-map": {
"version": "0.1.5",
"resolved": "https://registry.npmjs.org/es6-map/-/es6-map-0.1.5.tgz",
"integrity": "sha1-kTbgUD3MBqMBaQ8LsU/042TpSfA=",
"requires": {
"d": "1",
"es5-ext": "~0.10.14",
"es6-iterator": "~2.0.1",
"es6-set": "~0.1.5",
"es6-symbol": "~3.1.1",
"event-emitter": "~0.3.5"
}
},
"es6-promise": {
"version": "4.2.8",
"resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.8.tgz",
@@ -5011,11 +5069,33 @@
"es6-promise": "^4.0.3"
}
},
"es6-set": {
"version": "0.1.5",
"resolved": "https://registry.npmjs.org/es6-set/-/es6-set-0.1.5.tgz",
"integrity": "sha1-0rPsXU2ADO2BjbU40ol02wpzzLE=",
"requires": {
"d": "1",
"es5-ext": "~0.10.14",
"es6-iterator": "~2.0.1",
"es6-symbol": "3.1.1",
"event-emitter": "~0.3.5"
},
"dependencies": {
"es6-symbol": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/es6-symbol/-/es6-symbol-3.1.1.tgz",
"integrity": "sha1-vwDvT9q2uhtG7Le2KbTH7VcVzHc=",
"requires": {
"d": "1",
"es5-ext": "~0.10.14"
}
}
}
},
"es6-symbol": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/es6-symbol/-/es6-symbol-3.1.3.tgz",
"integrity": "sha512-NJ6Yn3FuDinBaBRWl/q5X/s4koRHBrgKAu+yGI6JCBeiu3qrcbJhwT2GeR/EXVfylRk8dpQVJoLEFhK+Mu31NA==",
"dev": true,
"requires": {
"d": "^1.0.1",
"ext": "^1.1.2"
@@ -5094,6 +5174,15 @@
"resolved": "https://registry.npmjs.org/eva-icons/-/eva-icons-1.1.3.tgz",
"integrity": "sha512-QBSEWNbEx1H0numXP1qgxKVCZHonRaky5ft4pGzQBcO4cy7mEja6TuJ8rc7BqX2pmkvetVQWKDH+DK/8y7GTag=="
},
"event-emitter": {
"version": "0.3.5",
"resolved": "https://registry.npmjs.org/event-emitter/-/event-emitter-0.3.5.tgz",
"integrity": "sha1-34xp7vFkeSPHFXuc6DhAYQsCzDk=",
"requires": {
"d": "1",
"es5-ext": "~0.10.14"
}
},
"eventemitter3": {
"version": "4.0.7",
"resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz",
@@ -5261,7 +5350,6 @@
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/ext/-/ext-1.4.0.tgz",
"integrity": "sha512-Key5NIsUxdqKg3vIsdw9dSuXpPCQ297y6wBjL30edxwPgt2E44WcWBZey/ZvUc6sERLTxKdyCu4gZFmUbk1Q7A==",
"dev": true,
"requires": {
"type": "^2.0.0"
},
@@ -5269,16 +5357,14 @@
"type": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/type/-/type-2.1.0.tgz",
"integrity": "sha512-G9absDWvhAWCV2gmF1zKud3OyC61nZDwWvBL2DApaVFogI07CprggiQAOOjvp2NRjYWFzPyu7vwtDrQFq8jeSA==",
"dev": true
"integrity": "sha512-G9absDWvhAWCV2gmF1zKud3OyC61nZDwWvBL2DApaVFogI07CprggiQAOOjvp2NRjYWFzPyu7vwtDrQFq8jeSA=="
}
}
},
"extend": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
"integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==",
"dev": true
"integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g=="
},
"extend-shallow": {
"version": "3.0.2",
@@ -5700,8 +5786,7 @@
"fs.realpath": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
"integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=",
"dev": true
"integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8="
},
"fsevents": {
"version": "2.1.3",
@@ -5782,7 +5867,6 @@
"version": "7.1.6",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz",
"integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==",
"dev": true,
"requires": {
"fs.realpath": "^1.0.0",
"inflight": "^1.0.4",
@@ -5801,6 +5885,42 @@
"is-glob": "^4.0.1"
}
},
"glob-stream": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/glob-stream/-/glob-stream-6.1.0.tgz",
"integrity": "sha1-cEXJlBOz65SIjYOrRtC0BMx73eQ=",
"requires": {
"extend": "^3.0.0",
"glob": "^7.1.1",
"glob-parent": "^3.1.0",
"is-negated-glob": "^1.0.0",
"ordered-read-streams": "^1.0.0",
"pumpify": "^1.3.5",
"readable-stream": "^2.1.5",
"remove-trailing-separator": "^1.0.1",
"to-absolute-glob": "^2.0.0",
"unique-stream": "^2.0.2"
},
"dependencies": {
"glob-parent": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-3.1.0.tgz",
"integrity": "sha1-nmr2KZ2NO9K9QEMIMr0RPfkGxa4=",
"requires": {
"is-glob": "^3.1.0",
"path-dirname": "^1.0.0"
}
},
"is-glob": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz",
"integrity": "sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo=",
"requires": {
"is-extglob": "^2.1.0"
}
}
}
},
"globals": {
"version": "11.12.0",
"resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz",
@@ -6052,6 +6172,17 @@
"minimalistic-assert": "^1.0.1"
}
},
"help-me": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/help-me/-/help-me-1.1.0.tgz",
"integrity": "sha1-jy1QjQYAtKRW2i8IZVbn5cBWo8Y=",
"requires": {
"callback-stream": "^1.0.2",
"glob-stream": "^6.1.0",
"through2": "^2.0.1",
"xtend": "^4.0.0"
}
},
"hex-color-regex": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/hex-color-regex/-/hex-color-regex-1.1.0.tgz",
@@ -6482,7 +6613,6 @@
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
"integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=",
"dev": true,
"requires": {
"once": "^1.3.0",
"wrappy": "1"
@@ -6655,6 +6785,15 @@
"integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==",
"dev": true
},
"is-absolute": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/is-absolute/-/is-absolute-1.0.0.tgz",
"integrity": "sha512-dOWoqflvcydARa360Gvv18DZ/gRuHKi2NU/wU5X1ZFzdYfH29nkiNZsF3mp4OJ3H4yo9Mx8A/uAGNzpzPN3yBA==",
"requires": {
"is-relative": "^1.0.0",
"is-windows": "^1.0.1"
}
},
"is-absolute-url": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/is-absolute-url/-/is-absolute-url-2.1.0.tgz",
@@ -6793,8 +6932,7 @@
"is-extglob": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
"integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=",
"dev": true
"integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI="
},
"is-fullwidth-code-point": {
"version": "2.0.0",
@@ -6817,6 +6955,11 @@
"integrity": "sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==",
"dev": true
},
"is-negated-glob": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/is-negated-glob/-/is-negated-glob-1.0.0.tgz",
"integrity": "sha1-aRC8pdqMleeEtXUbl2z1oQ/uNtI="
},
"is-negative-zero": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.0.tgz",
@@ -6887,6 +7030,14 @@
"has-symbols": "^1.0.1"
}
},
"is-relative": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/is-relative/-/is-relative-1.0.0.tgz",
"integrity": "sha512-Kw/ReK0iqwKeu0MITLFuj0jbPAmEiOsIwyIXvvbfa6QfmN9pkD1M+8pdk7Rl/dTKbH34/XBFMbgD4iMJhLQbGA==",
"requires": {
"is-unc-path": "^1.0.0"
}
},
"is-resolvable": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/is-resolvable/-/is-resolvable-1.1.0.tgz",
@@ -6928,11 +7079,18 @@
"integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=",
"dev": true
},
"is-unc-path": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/is-unc-path/-/is-unc-path-1.0.0.tgz",
"integrity": "sha512-mrGpVd0fs7WWLfVsStvgF6iEJnbjDFZh9/emhRDcGWTduTfNHd9CHeUwH3gYIjdbwo4On6hunkztwOaAw0yllQ==",
"requires": {
"unc-path-regex": "^0.1.2"
}
},
"is-windows": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz",
"integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==",
"dev": true
"integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA=="
},
"is-wsl": {
"version": "2.2.0",
@@ -7219,6 +7377,11 @@
"integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
"dev": true
},
"json-stable-stringify-without-jsonify": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz",
"integrity": "sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE="
},
"json-stringify-safe": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz",
@@ -8246,7 +8409,6 @@
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz",
"integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==",
"dev": true,
"requires": {
"brace-expansion": "^1.1.7"
}
@@ -8390,11 +8552,50 @@
}
}
},
"mqtt": {
"version": "4.2.1",
"resolved": "https://registry.npmjs.org/mqtt/-/mqtt-4.2.1.tgz",
"integrity": "sha512-Iv893r+jWlo5GkNcPOfCGwW8M49IixwHiKLFFYTociEymSibUVCORVEjPXWPGzSxhn7BdlUeHicbRmWiv0Crkg==",
"requires": {
"base64-js": "^1.3.0",
"commist": "^1.0.0",
"concat-stream": "^1.6.2",
"debug": "^4.1.1",
"end-of-stream": "^1.4.1",
"es6-map": "^0.1.5",
"help-me": "^1.0.1",
"inherits": "^2.0.3",
"minimist": "^1.2.5",
"mqtt-packet": "^6.3.2",
"pump": "^3.0.0",
"readable-stream": "^2.3.6",
"reinterval": "^1.1.0",
"split2": "^3.1.0",
"ws": "^7.3.1",
"xtend": "^4.0.1"
},
"dependencies": {
"ws": {
"version": "7.3.1",
"resolved": "https://registry.npmjs.org/ws/-/ws-7.3.1.tgz",
"integrity": "sha512-D3RuNkynyHmEJIpD2qrgVkc9DQ23OrN/moAwZX4L8DfvszsJxpjQuUq3LMx6HoYji9fbIOBY18XWBsAux1ZZUA=="
}
}
},
"mqtt-packet": {
"version": "6.6.0",
"resolved": "https://registry.npmjs.org/mqtt-packet/-/mqtt-packet-6.6.0.tgz",
"integrity": "sha512-LvghnKMFC70hKWMVykmhJarlO5e7lT3t9s9A2qPCUx+lazL3Mq55U+eCV0eLi7/nRRQYvEUWo/2tTo89EjnCJQ==",
"requires": {
"bl": "^4.0.2",
"debug": "^4.1.1",
"process-nextick-args": "^2.0.1"
}
},
"ms": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
"dev": true
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
},
"multicast-dns": {
"version": "6.2.3",
@@ -8464,8 +8665,7 @@
"next-tick": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.0.0.tgz",
"integrity": "sha1-yobR/ogoFpsBICCOPchCS524NCw=",
"dev": true
"integrity": "sha1-yobR/ogoFpsBICCOPchCS524NCw="
},
"ng2-charts": {
"version": "2.4.2",
@@ -8486,6 +8686,22 @@
"tslib": "^2.0.0"
}
},
"ngx-mqtt": {
"version": "7.0.14",
"resolved": "https://registry.npmjs.org/ngx-mqtt/-/ngx-mqtt-7.0.14.tgz",
"integrity": "sha512-b6TwC5m8eSGbKEF/I6dDpPTRPtMm3uKKxVKanqiF+JenOlNC5GKJ7YlXLpzCGHn+pK+OuXxSlRmtNbAegANl/w==",
"requires": {
"mqtt": "4.2.1",
"tslib": "^1.10.0"
},
"dependencies": {
"tslib": {
"version": "1.14.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
"integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="
}
}
},
"nice-try": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz",
@@ -8983,7 +9199,6 @@
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
"integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=",
"dev": true,
"requires": {
"wrappy": "1"
}
@@ -9106,6 +9321,14 @@
}
}
},
"ordered-read-streams": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/ordered-read-streams/-/ordered-read-streams-1.0.1.tgz",
"integrity": "sha1-d8DLN8QVJdZBZtmQ/61+xqDhNj4=",
"requires": {
"readable-stream": "^2.0.1"
}
},
"original": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/original/-/original-1.0.2.tgz",
@@ -9480,8 +9703,7 @@
"path-dirname": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/path-dirname/-/path-dirname-1.0.2.tgz",
"integrity": "sha1-zDPSTVJeCZpTiMAzbG4yuRYGCeA=",
"dev": true
"integrity": "sha1-zDPSTVJeCZpTiMAzbG4yuRYGCeA="
},
"path-exists": {
"version": "3.0.0",
@@ -9492,8 +9714,7 @@
"path-is-absolute": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
"integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=",
"dev": true
"integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18="
},
"path-is-inside": {
"version": "1.0.2",
@@ -10714,7 +10935,6 @@
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz",
"integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==",
"dev": true,
"requires": {
"end-of-stream": "^1.1.0",
"once": "^1.3.1"
@@ -10724,7 +10944,6 @@
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/pumpify/-/pumpify-1.5.1.tgz",
"integrity": "sha512-oClZI37HvuUJJxSKKrC17bZ9Cu0ZYhEAGPsPUy9KlMUmv9dKX2o77RUmq7f3XjIxbwyGwYzbzQ1L2Ks8sIradQ==",
"dev": true,
"requires": {
"duplexify": "^3.6.0",
"inherits": "^2.0.3",
@@ -10735,7 +10954,6 @@
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/pump/-/pump-2.0.1.tgz",
"integrity": "sha512-ruPMNRkN3MHP1cWJc9OWr+T/xDP0jhXYCLfJcBuX54hhfIBnaQmAUMfDcG4DM5UMWByBbJY69QSphm3jtDKIkA==",
"dev": true,
"requires": {
"end-of-stream": "^1.1.0",
"once": "^1.3.1"
@@ -11149,11 +11367,15 @@
}
}
},
"reinterval": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/reinterval/-/reinterval-1.1.0.tgz",
"integrity": "sha1-M2Hs+jymwYKDOA3Qu5VG85D17Oc="
},
"remove-trailing-separator": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz",
"integrity": "sha1-wkvOKig62tW8P1jg1IJJuSN52O8=",
"dev": true
"integrity": "sha1-wkvOKig62tW8P1jg1IJJuSN52O8="
},
"repeat-element": {
"version": "1.1.3",
@@ -12177,40 +12399,6 @@
"websocket-driver": "0.6.5"
}
},
"sockjs-client": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/sockjs-client/-/sockjs-client-1.4.0.tgz",
"integrity": "sha512-5zaLyO8/nri5cua0VtOrFXBPK1jbL4+1cebT/mmKA1E1ZXOvJrII75bPu0l0k843G/+iAbhEqzyKr0w/eCCj7g==",
"dev": true,
"requires": {
"debug": "^3.2.5",
"eventsource": "^1.0.7",
"faye-websocket": "~0.11.1",
"inherits": "^2.0.3",
"json3": "^3.3.2",
"url-parse": "^1.4.3"
},
"dependencies": {
"debug": {
"version": "3.2.6",
"resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz",
"integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==",
"dev": true,
"requires": {
"ms": "^2.1.1"
}
},
"faye-websocket": {
"version": "0.11.3",
"resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.11.3.tgz",
"integrity": "sha512-D2y4bovYpzziGgbHYtGCMjlJM36vAl/y+xUyn1C+FVx8szd1E+86KwVw6XvYSzOP8iMpm1X0I4xJD+QtUb36OA==",
"dev": true,
"requires": {
"websocket-driver": ">=0.5.1"
}
}
}
},
"socks": {
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/socks/-/socks-2.3.3.tgz",
@@ -12413,6 +12601,26 @@
"extend-shallow": "^3.0.0"
}
},
"split2": {
"version": "3.2.2",
"resolved": "https://registry.npmjs.org/split2/-/split2-3.2.2.tgz",
"integrity": "sha512-9NThjpgZnifTkJpzTZ7Eue85S49QwpNhZTq6GRJwObb6jnLFNGB7Qm73V5HewTROPyxD0C29xqmaI68bQtV+hg==",
"requires": {
"readable-stream": "^3.0.0"
},
"dependencies": {
"readable-stream": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz",
"integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==",
"requires": {
"inherits": "^2.0.3",
"string_decoder": "^1.1.1",
"util-deprecate": "^1.0.1"
}
}
}
},
"sprintf-js": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
@@ -12514,8 +12722,7 @@
"stream-shift": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.1.tgz",
"integrity": "sha512-AiisoFqQ0vbGcZgQPY1cdP2I76glaVA/RauYR4G4thNFgkTqr90yXTo4LYX60Jl+sIlPNHHdGSwo01AvbKUSVQ==",
"dev": true
"integrity": "sha512-AiisoFqQ0vbGcZgQPY1cdP2I76glaVA/RauYR4G4thNFgkTqr90yXTo4LYX60Jl+sIlPNHHdGSwo01AvbKUSVQ=="
},
"streamroller": {
"version": "2.2.4",
@@ -12928,12 +13135,20 @@
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz",
"integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==",
"dev": true,
"requires": {
"readable-stream": "~2.3.6",
"xtend": "~4.0.1"
}
},
"through2-filter": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/through2-filter/-/through2-filter-3.0.0.tgz",
"integrity": "sha512-jaRjI2WxN3W1V8/FMZ9HKIBXixtiqs3SQSX4/YGIiP3gL6djW48VoZq9tDqeCWs3MT8YY5wb/zli8VW8snY1CA==",
"requires": {
"through2": "~2.0.0",
"xtend": "~4.0.0"
}
},
"thunky": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/thunky/-/thunky-1.1.0.tgz",
@@ -12974,6 +13189,15 @@
"os-tmpdir": "~1.0.2"
}
},
"to-absolute-glob": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/to-absolute-glob/-/to-absolute-glob-2.0.2.tgz",
"integrity": "sha1-GGX0PZ50sIItufFFt4z/fQ98hJs=",
"requires": {
"is-absolute": "^1.0.0",
"is-negated-glob": "^1.0.0"
}
},
"to-array": {
"version": "0.1.4",
"resolved": "https://registry.npmjs.org/to-array/-/to-array-0.1.4.tgz",
@@ -13169,8 +13393,7 @@
"type": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/type/-/type-1.2.0.tgz",
"integrity": "sha512-+5nt5AAniqsCnu2cEQQdpzCAh33kVx8n0VoFidKpB1dVVLAN/F+bgVOqOJqOnEnrhp222clB5p3vUlD+1QAnfg==",
"dev": true
"integrity": "sha512-+5nt5AAniqsCnu2cEQQdpzCAh33kVx8n0VoFidKpB1dVVLAN/F+bgVOqOJqOnEnrhp222clB5p3vUlD+1QAnfg=="
},
"type-detect": {
"version": "4.0.8",
@@ -13210,6 +13433,11 @@
"integrity": "sha512-+O8/qh/Qj8CgC6eYBVBykMrNtp5Gebn4dlGD/kKXVkJNDwyrAwSIqwz8CDf+tsAIWVycKcku6gIXJ0qwx/ZXaQ==",
"dev": true
},
"unc-path-regex": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/unc-path-regex/-/unc-path-regex-0.1.2.tgz",
"integrity": "sha1-5z3T17DXxe2G+6xrCufYxqadUPo="
},
"unicode-canonical-property-names-ecmascript": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-1.0.4.tgz",
@@ -13280,6 +13508,15 @@
"imurmurhash": "^0.1.4"
}
},
"unique-stream": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/unique-stream/-/unique-stream-2.3.1.tgz",
"integrity": "sha512-2nY4TnBE70yoxHkDli7DMazpWiP7xMdCYqU2nBRO0UB+ZpEkGsSija7MvmvnZFUeC+mrgiUfcHSr3LmRFIg4+A==",
"requires": {
"json-stable-stringify-without-jsonify": "^1.0.1",
"through2-filter": "^3.0.0"
}
},
"universal-analytics": {
"version": "0.4.23",
"resolved": "https://registry.npmjs.org/universal-analytics/-/universal-analytics-0.4.23.tgz",
@@ -14273,6 +14510,15 @@
"upath": "^1.1.1"
}
},
"faye-websocket": {
"version": "0.11.3",
"resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.11.3.tgz",
"integrity": "sha512-D2y4bovYpzziGgbHYtGCMjlJM36vAl/y+xUyn1C+FVx8szd1E+86KwVw6XvYSzOP8iMpm1X0I4xJD+QtUb36OA==",
"dev": true,
"requires": {
"websocket-driver": ">=0.5.1"
}
},
"fill-range": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz",
@@ -14408,6 +14654,31 @@
"integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==",
"dev": true
},
"sockjs-client": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/sockjs-client/-/sockjs-client-1.4.0.tgz",
"integrity": "sha512-5zaLyO8/nri5cua0VtOrFXBPK1jbL4+1cebT/mmKA1E1ZXOvJrII75bPu0l0k843G/+iAbhEqzyKr0w/eCCj7g==",
"dev": true,
"requires": {
"debug": "^3.2.5",
"eventsource": "^1.0.7",
"faye-websocket": "~0.11.1",
"inherits": "^2.0.3",
"json3": "^3.3.2",
"url-parse": "^1.4.3"
},
"dependencies": {
"debug": {
"version": "3.2.6",
"resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz",
"integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==",
"dev": true,
"requires": {
"ms": "^2.1.1"
}
}
}
},
"supports-color": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.1.0.tgz",
@@ -14599,8 +14870,7 @@
"wrappy": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
"integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=",
"dev": true
"integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8="
},
"ws": {
"version": "6.2.1",

View File

@@ -30,6 +30,7 @@
"moment": "^2.29.1",
"ng2-charts": "^2.4.2",
"ngx-mapbox-gl": "^4.8.1",
"ngx-mqtt": "^7.0.14",
"rxjs": "~6.6.0",
"tslib": "^2.0.0",
"zone.js": "~0.10.2"

View File

@@ -1,12 +1,15 @@
import { HttpClient, HttpHeaders, HttpParams } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { MqttService } from 'ngx-mqtt';
import { BehaviorSubject } from 'rxjs';
import * as moment from 'moment';
export interface ILogin {
token: string;
}
export interface IBeat {
_id: string;
coordinate?: number[];
accuracy: number;
speed: number;
@@ -35,8 +38,7 @@ export enum UserType {
export interface IUser {
name: string;
password: string;
salt: string;
brokerToken: string;
type: UserType;
lastLogin: Date;
twoFASecret?: string;
@@ -67,8 +69,15 @@ export class APIService {
private token: string;
username: string;
time: ITimespan | undefined;
rabbitmq: any;
time: ITimespan | undefined = {
from: moment().subtract(1, 'day').unix(),
to: moment().unix()
};
// Passthough data (not useful for api but a way for components to share data)
showFilter = true;
maxAccuracy: BehaviorSubject<number> = new BehaviorSubject(30);
// Cached data
beats: IBeat[];
@@ -79,9 +88,8 @@ export class APIService {
phones: IPhone[];
user: IUser = {
name: '',
brokerToken: '',
lastLogin: new Date(2020, 3, 1),
password: '',
salt: '',
type: UserType.GUEST,
createdAt: new Date(),
twoFASecret: ''
@@ -95,7 +103,7 @@ export class APIService {
API_ENDPOINT = 'http://192.168.178.26:8040';
constructor(private httpClient: HttpClient) { }
constructor(private httpClient: HttpClient, private mqtt: MqttService) {}
async login(username: string, password: string): Promise<ILogin> {
return new Promise<ILogin>(async (resolve, reject) => {
@@ -107,6 +115,25 @@ export class APIService {
this.username = username;
await this.getPhones();
await this.getUserInfo();
// Connect with RabbitMQ after we received our user information
this.mqtt.connect({
hostname: '192.168.178.26',
port: 15675,
protocol: 'ws',
path: '/ws',
username: this.user.name,
password: this.user.brokerToken
});
this.mqtt.observe('/').subscribe(message => {
if (this.beats !== undefined) {
this.beats.push(JSON.parse(message.payload.toString()) as IBeat);
this.beatsEvent.next(this.beats);
this.beatStats.totalBeats++;
}
});
await this.getBeats();
await this.getBeatStats();
this.loginEvent.next(true);

View File

@@ -14,6 +14,7 @@ import { LoginComponent } from './login/login.component';
import { MapComponent } from './map/map.component';
import { UserComponent } from './user/user.component';
import { DashboardWidgetComponent } from './dashboard-widget/dashboard-widget.component';
import { IMqttServiceOptions, MqttModule } from 'ngx-mqtt';
@NgModule({
declarations: [
@@ -31,6 +32,7 @@ import { DashboardWidgetComponent } from './dashboard-widget/dashboard-widget.co
BrowserAnimationsModule,
FormsModule,
HttpClientModule,
MqttModule.forRoot({}),
NgxMapboxGLModule.withConfig({
accessToken: 'pk.eyJ1IjoibW9uZGVpMSIsImEiOiJja2dsY2ZtaG0xZ2o5MnR0ZWs0Mm82OTBpIn0.NzDWN3P6jJLmci_v3MM1tA'
}),

View File

@@ -1,6 +1,7 @@
import { AfterViewInit, Component, OnInit } from '@angular/core';
import { ChartDataSets, ChartOptions } from 'chart.js';
import { Label } from 'ng2-charts';
import * as moment from 'moment';
import { APIService, IBeat } from '../api.service';
@Component({
@@ -51,10 +52,12 @@ export class DashboardComponent implements AfterViewInit {
this.lineChartLabels = [];
const batteryLevels: number[] = [];
let currentLevel = 0;
const finalBeats = beats.filter((val, i, array) => {
if (batteryLevels.indexOf(val.battery) === -1) {
if (currentLevel !== val.battery) {
batteryLevels.push(val.battery);
currentLevel = val.battery;
return true;
} else {
return false;
@@ -63,7 +66,7 @@ export class DashboardComponent implements AfterViewInit {
finalBeats.forEach((beat) => {
this.lineChartData[0].data.push(beat.battery);
this.lineChartLabels.push(this.formatDateTime(new Date(beat.createdAt)));
this.lineChartLabels.push(moment(new Date(beat.createdAt)).format(this.lineChartOptions.scales.xAxes[0].time.parser.toString()));
});
let tDistance = 0;
@@ -83,10 +86,6 @@ export class DashboardComponent implements AfterViewInit {
});
}
private formatDateTime(date: Date): string {
return `${date.getMonth()}/${date.getDay()}/${date.getFullYear()} ${date.getHours()}:${date.getMinutes()}:${date.getSeconds()}`;
}
ngAfterViewInit(): void {
}

View File

@@ -1,5 +1,7 @@
<div id="filter" [ngClass]="{hide: !this.api.showFilter}" *ngIf="this.api.loginEvent.value">
<h3 (click)="update()" style="cursor: pointer;">Refresh</h3>
<!-- Time range -->
<select (change)="update($event.target.value)" [(ngModel)]="this.presetHours">
<option value="-1">Today</option>
<option value="3">Last 3h</option>
@@ -17,4 +19,8 @@
<option value="month">Months</option>
<option value="year">Years</option>
</select>
<!-- Max accuracy -->
<h4>Max accuracy</h4>
<input class="customRange" (change)="updateAccuracy($event)">
</div>

View File

@@ -40,6 +40,10 @@ export class FilterComponent implements OnInit {
this.refresh();
}
updateAccuracy(val: any): void {
this.api.maxAccuracy.next(Number(val.target.value));
}
async refresh(): Promise<void> {
await this.api.getBeats();
await this.api.getBeatStats();

View File

@@ -1,5 +1,6 @@
<mgl-map [style]="'mapbox://styles/mapbox/dark-v10'" [zoom]="[15]" [center]="[this.lastLocation[0], this.lastLocation[1]]" *ngIf="showMap">
<mgl-geojson-source id="locHistory" [data]="data"></mgl-geojson-source>
<mgl-geojson-source id="locHistoryFiltered" [data]="mostVisitData"></mgl-geojson-source>
<mgl-geojson-source id="lastLoc" [data]="lastLocationData"></mgl-geojson-source>
<mgl-layer
id="locHistory"
@@ -10,6 +11,12 @@
'line-width': 3
}"
></mgl-layer>
<mgl-layer
id="locHistoryHeatmap"
type="heatmap"
source="locHistoryFiltered"
[paint]="mostVisitPaint"
></mgl-layer>
<mgl-layer
id="lastLoc"
type="circle"

View File

@@ -1,5 +1,4 @@
import { AfterViewInit, Component, OnInit } from '@angular/core';
import { Map } from 'mapbox-gl';
import { APIService } from '../api.service';
@Component({
@@ -7,9 +6,8 @@ import { APIService } from '../api.service';
templateUrl: './map.component.html',
styleUrls: ['./map.component.scss']
})
export class MapComponent implements AfterViewInit {
export class MapComponent {
map: Map;
lastLocation: number[] = [0, 0];
showMap = false;
@@ -21,6 +19,79 @@ export class MapComponent implements AfterViewInit {
geometry: { type: 'LineString', coordinates: [] }
}]
};
mostVisitData: GeoJSON.FeatureCollection<GeoJSON.LineString> = {
type: 'FeatureCollection',
features: [{
type: 'Feature',
properties: null,
geometry: { type: 'LineString', coordinates: [] }
}]
};
mostVisitPaint = {
// Increase the heatmap weight based on frequency and property magnitude
'heatmap-weight': [
'interpolate',
['linear'],
['get', 'mag'],
0,
0,
6,
1
],
// Increase the heatmap color weight weight by zoom level
// heatmap-intensity is a multiplier on top of heatmap-weight
'heatmap-intensity': [
'interpolate',
['linear'],
['zoom'],
0,
3,
9,
1
],
// Color ramp for heatmap. Domain is 0 (low) to 1 (high).
// Begin color ramp at 0-stop with a 0-transparancy color
// to create a blur-like effect.
'heatmap-color': [
'interpolate',
['linear'],
['heatmap-density'],
0,
'rgba(33,102,172,0)',
0.3,
'rgb(103,169,207)',
0.4,
'rgb(209,229,240)',
0.7,
'rgb(253,219,199)',
0.95,
'rgb(239,138,98)',
1,
'rgb(178,24,43)'
],
// Adjust the heatmap radius by zoom level
'heatmap-radius': [
'interpolate',
['linear'],
['zoom'],
0,
2,
7,
10,
9,
15
],
// Transition from heatmap to circle layer by zoom level
'heatmap-opacity': [
'interpolate',
['linear'],
['zoom'],
16,
1,
17,
0
]
};
lastLocationData: GeoJSON.FeatureCollection<GeoJSON.Point> = {
type: 'FeatureCollection', features: [
@@ -54,33 +125,44 @@ export class MapComponent implements AfterViewInit {
this.api.beatsEvent.subscribe(beats => {
if (beats.length === 0) { return; }
this.lastLocationPaint['circle-radius'].stops[1][1] = this.metersToPixelsAtMaxZoom(
beats[0].accuracy, this.lastLocation[0]
);
this.update();
this.lastLocationPaint['circle-radius'].stops[1][1] = this.metersToPixelsAtMaxZoom(
beats[beats.length - 1].accuracy, this.lastLocation[0]
);
this.lastLocationPaint = { ...this.lastLocationPaint };
this.api.maxAccuracy.subscribe(val => {
this.buildMap(val);
});
});
}
/* Function to draw circle with exact size by
/* Function to draw circle with exact size from
https://stackoverflow.com/questions/37599561/drawing-a-circle-with-the-radius-in-miles-meters-with-mapbox-gl-js
*/
private metersToPixelsAtMaxZoom(meters: number, latitude: number): number {
return meters / 0.075 / Math.cos(latitude * Math.PI / 180);
}
/* Function to find out if a provided point is in the area from
https://stackoverflow.com/questions/24680247/check-if-a-latitude-and-longitude-is-within-a-circle-google-maps
*/
private isPointInRadius(checkPoint: { lat: number, lng: number }, centerPoint: { lat: number, lng: number }, km: number): boolean {
const ky = 40000 / 360;
const kx = Math.cos(Math.PI * centerPoint.lat / 180.0) * ky;
const dx = Math.abs(centerPoint.lng - checkPoint.lng) * kx;
const dy = Math.abs(centerPoint.lat - checkPoint.lat) * ky;
return Math.sqrt(dx * dx + dy * dy) <= km;
}
async update(): Promise<void> {
this.data.features[0].geometry.coordinates = [];
// Add lines to map backwards (because it looks cool)
for (let i = this.api.beats.length - 1; i >= 0; i--) {
const beat = this.api.beats[i];
this.data.features[0].geometry.coordinates.push([beat.coordinate[1], beat.coordinate[0]]);
this.data = { ... this.data };
}
console.log('Last', this.api.beats[0]);
this.buildMap();
this.lastLocation = [ this.api.beats[0].coordinate[1],
this.api.beats[0].coordinate[0] ];
this.lastLocation = [this.api.beats[this.api.beats.length - 1].coordinate[1],
this.api.beats[this.api.beats.length - 1].coordinate[0]];
this.lastLocationData.features[0].geometry.coordinates = this.lastLocation;
this.lastLocationData = { ...this.lastLocationData };
@@ -88,7 +170,46 @@ export class MapComponent implements AfterViewInit {
this.showMap = true;
}
async ngAfterViewInit(): Promise<void> {
buildMap(maxAccuracy: number = 30): void {
const mostVisit = new Map<string, number>();
this.data.features[0].geometry.coordinates = [];
this.mostVisitData.features[0].geometry.coordinates = [];
for (let i = 0; i < this.api.beats.length; i++) {
const beat = this.api.beats[i];
if (beat.accuracy > maxAccuracy) { continue; }
this.data.features[0].geometry.coordinates.push([beat.coordinate[1], beat.coordinate[0]]);
// Get most visit points
for (let b = 0; b < this.api.beats.length; b++) {
const beat2 = this.api.beats[b];
const isNearPoint = this.isPointInRadius(
{ lat: beat2.coordinate[0], lng: beat2.coordinate[1] },
{ lat: beat.coordinate[0], lng: beat.coordinate[1] },
0.02
);
if (isNearPoint) {
if (mostVisit.has(beat2._id)) {
mostVisit.set(beat2._id, mostVisit.get(beat2._id) + 1);
} else {
mostVisit.set(beat2._id, 1);
}
}
}
}
for (const [key, value] of mostVisit) {
if (value < 2) { continue; }
this.api.beats.forEach(beat => {
if (beat._id === key) {
this.mostVisitData.features[0].geometry.coordinates.push([beat.coordinate[1], beat.coordinate[0]]);
}
});
}
this.data = { ... this.data };
this.mostVisitData = { ... this.mostVisitData };
}
}

View File

@@ -1,6 +1,7 @@
@import '../../styles.scss';
#user {
min-width: 40rem;
margin-top: 3rem;
margin-left: 20rem;
margin-right: 20rem;