wouterbulten/slacjs

View on GitHub
src/app/app-replay.js

Summary

Maintainability
A
3 hrs
Test Coverage
import config from './config';
import SlacController from './slac-controller';
import ReplayRenderer from './view/replay-renderer';
import { degreeToNormalisedHeading } from './util/motion';

/**
 * Application object for replaying recorded data
 * @type {Object}
 */
window.SlacApp = {

    bleEventIteration: 0,
    motionEventIteration: 0,

    controller: undefined,
    renderer: undefined,

    bleInterval: undefined,
    motionInterval: undefined,

    lastUpdate: 0,

    startMotionTimestamp: 0,
    currentMotionTimestamp: 0,

    startHeading: 0,

    distPlots: {},

    errorPlot: {},

    error: {avg: 0},

    initialize() {

        //Reset all the timers and counters so that we can reuse this object
        //on multiple runs
        this.bleEventIteration = 0;
        this.motionEventIteration = 0;

        this.lastUpdate = 0;

        this.startMotionTimestamp = 0;
        this.currentMotionTimestamp = 0;

        this.startHeading = 0;

        this.error = {avg: 0};

        if (typeof SlacJsData === 'undefined') {
            console.error('No replay data found');
        }

        if (typeof SlacJsLandmarkPositions === 'undefined') {
            console.error('No true landmark positions found');
        }

        if (typeof SlacJsStartingPosition === 'undefined') {
            console.error('No starting position found');
        }

        if (config.replay.showVisualisation)
        {
            //Create a renderer for the canvas view
            this.renderer = new ReplayRenderer('slacjs-map', SlacJsLandmarkPositions);
        }

        //Create plot for the errors
        //Only when we actually show the progress of the algorithm
        if (config.replay.delayAlgorithm) {
            this._initErrorPlot();
        }
    },

    start() {

        //Store the current heading
        this.startHeading = SlacJsData.motion[0].heading;

        //Update the initial pose with the true starting position
        config.particles.user.defaultPose.x = SlacJsStartingPosition.x;
        config.particles.user.defaultPose.y = SlacJsStartingPosition.y;

        //Create a new controller
        this.controller = new SlacController(config);

        //We hack the controller to update the BLE observations before we run the internal update function
        this.controller.pedometer.onStep(() => {

            this._updateBleObservations(this.currentMotionTimestamp);
            this.controller._update();

            //Take the last observations and output the measurement error
            //for the best particle
            const user = this.controller.particleSet.userEstimate();

            //Show the current error
            this._calculateLandmarkError();
        });

        //Add a listener to the sensor of the controller
        this.controller.sensor.setEventListener((uid, name, event, msg) => {

            if (event != 'update') {
                console.log(`[SLCAjs/sensor] ${uid} ${event}, message: "${msg}"`);
            }
        });

        this.controller.start();

        //Bind renderer to controller
        this.controller.onUpdate((particles) => {

            if (config.replay.showVisualisation)
            {
                //Update the canvas
                this.renderer.render(particles);
            }
        });

        //Save the start time, we use this to determine which BLE events to send
        this.lastUpdate = new Date().getTime();

        //Run the algorithm real time or as fast as possible
        if (config.replay.delayAlgorithm) {
            this.motionInterval = setInterval(() => this._processMotionObservation(), config.replay.updateRate);
        }
        else {

            let hasNext = true;

            while (hasNext) {
                hasNext = this._processMotionObservation()
            }
        }
    },

    /**
     * Utility function that returns the true distance to a beacon given a x,y position
     * @param  {Number} x         Location of user
     * @param  {Number} y         Location of user
     * @param  {String} name      Beacon name
     * @return {Number}           Distance
     */
    distanceToBeacon(x, y, name) {

        const beacon = SlacJsLandmarkPositions[name];

        const dx = x - beacon.x;
        const dy = y - beacon.y;

        return Math.sqrt((dx * dx) + (dy * dy));
    },

    /**
     * Simulate a motion event
     * @return {Boolean}
     */
    _processMotionObservation() {

        if (this.motionEventIteration >= SlacJsData.motion.length) {
            clearInterval(this.motionInterval);

            console.log('[SLACjs] Motion events finished');
            return false;
        }

        const data = SlacJsData.motion[this.motionEventIteration];

        if (this.startMotionTimestamp === 0) {
            this.startMotionTimestamp = data.timestamp;
        }

        this.controller.addMotionObservation(
            data.x, data.y, data.z,
            degreeToNormalisedHeading(data.heading, this.startHeading)
        );

        this.currentMotionTimestamp = data.timestamp;
        this.motionEventIteration++;

        return true;
    },

    /**
     * Process all BLE observations until timestamp
     * @param  {Number} timestamp
     * @return {void}
     */
    _updateBleObservations(timestamp) {

        let current;

        do {
            current = SlacJsData.bluetooth[this.bleEventIteration];

            this.bleEventIteration++;

            this.controller.addDeviceObservation(current.address, current.rssi, current.name);
        }
        while (current.timestamp <= timestamp);
    },

    /**
     * Calculate the landmark error and show on screen
     * @return {[type]} [description]
     */
    _calculateLandmarkError() {

        const distArr = [];
        let landmarkErrorsStr = '';

        this.controller.particleSet.bestParticle().landmarks.forEach((l) => {

            const trueL = SlacJsLandmarkPositions[l.name];

            const dist = Math.sqrt(Math.pow(trueL.x - l.x, 2) + Math.pow(trueL.y - l.y, 2));

            distArr.push(dist);
            this.error[l.name] = dist;

            landmarkErrorsStr += l.name + ': ' + dist + '<br>';
        });

        if (distArr.length > 0) {
            $('.landmark-individual-error').html(landmarkErrorsStr);

            const avg = distArr.reduce(function(total, d) { return total + d; }, 0) / distArr.length;

            //Only update the plot when we actually show the progress of the algorithm
            if (config.replay.delayAlgorithm) {
                this.errorPlot.data.push(avg);

                this.errorPlot.plot.series[0].setData(this.errorPlot.data);
            }

            $('.landmark-error').html(avg);
            this.error.avg = avg;
        }
    },

    /**
     * Initialize the error plot
     * @return {void}
     */
    _initErrorPlot() {

        this.errorPlot = {

            data: [],

            plot: new Highcharts.Chart({
                chart: {
                    renderTo: 'error-plot'
                },
                title: {
                    text: 'Error plot'
                },
                xAxis: {
                    title: {
                        text: 'Time'
                    }
                },
                yAxis: [
                    {
                        title: {
                            text: 'Error'
                        },
                        max: 4,
                        min: 1
                    }
                ],
                series: [
                    {
                        name: 'Error',
                        type: 'line'
                    }
                ]
            })
        }
    }
};