<commit_before>// ==UserScript==
// @name         agar-mini-map
// @namespace    http://github.com/dimotsai/
// @version      0.30
// @description  This script will show a mini map and your location on agar.io
// @author       dimotsai
// @license      MIT
// @match        http://agar.io/
// @grant        none
// @run-at       document-body
// ==/UserScript==

(function() {
    var _WebSocket = window.WebSocket;
    var $ = window.jQuery;

    var cells = [];
    var my_cell_ids = [];

    var options = {
        enableMultiCells: true,
        enablePosition: true
    };

    function miniMapCreateToken(id, color) {
        var mini_map_token = $('<div>').attr('id', 'mini-map-token-' + id).css({
            position: 'absolute',
            width: '5%',
            height: '5%',
            background: color,
            top: '0%',
            left: '0%',
            borderRadius: '50%'
        });
        return mini_map_token;
    }

    function miniMapRegisterToken(id, token) {
        if (window.mini_map_tokens[id] === undefined) {
            window.mini_map.append(token);
            window.mini_map_tokens[id] = token;
        }
    }

    function miniMapUnregisterToken(id) {
        if (window.mini_map_tokens[id] !== undefined) {
            window.mini_map_tokens[id].detach();
            delete window.mini_map_tokens[id];
        }
    }

    function miniMapIsRegisteredToken(id) {
        return window.mini_map_tokens[id] !== undefined;
    }

    function miniMapUpdateToken(id, x, y, size) {
        if (window.mini_map_tokens[id] !== undefined) {
            window.mini_map_tokens[id]
            .css({
                left:   x    * 100 + '%',
                top:    y    * 100 + '%',
                width:  size * 100 + '%',
                height: size * 100 + '%'
            });
            return true;
        } else {
            return false;
        }
    }

    function miniMapUpdatePos(x, y) {
        window.mini_map_pos.text('x: ' + x.toFixed(0) + ', y: ' + y.toFixed(0));
    }

    function miniMapInit() {
        var $ = window.jQuery;
        window.mini_map_tokens = {};

        if ($('#mini-map-wrapper').length === 0) {
            var wrapper = $('<div>').attr('id', 'mini-map-wrapper').css({
                position: 'fixed',
                bottom: 10,
                right: 10,
                width: 300,
                height: 300,
                background: 'rgba(128, 128, 128, 0.58)'
            });

            var mini_map = $('<div>').attr('id', 'mini-map').css({
                width: '100%',
                height: '100%',
                position: 'relative'
            });

            wrapper.append(mini_map).appendTo(document.body);

            window.mini_map = mini_map;
        }

        if ($('#mini-map-pos').length === 0) {
            window.mini_map_pos = $('<div>').attr('id', 'mini-map-pos').css({
                bottom: 10,
                right: 10,
                color: 'white',
                fontSize: 15,
                fontWeight: 800,
                position: 'fixed',
                padding: '0px 10px'
            }).appendTo(document.body);
        }

    }

    // cell constructor
    function Cell(id, x, y, size, color, name) {
        cells[id] = this;
        this.id = id;
        this.ox = this.x = x;
        this.oy = this.y = y;
        this.oSize = this.size = size;
        this.color = color;
        this.points = [];
        this.pointsAcc = [];
        this.setName(name);
    }

    Cell.prototype = {
        id: 0,
        points: null,
        pointsAcc: null,
        name: null,
        nameCache: null,
        sizeCache: null,
        x: 0,
        y: 0,
        size: 0,
        ox: 0,
        oy: 0,
        oSize: 0,
        nx: 0,
        ny: 0,
        nSize: 0,
        updateTime: 0,
        updateCode: 0,
        drawTime: 0,
        destroyed: false,
        isVirus: false,
        isAgitated: false,
        wasSimpleDrawing: true,

        destroy: function() {
            delete cells[this.id];
            id = my_cell_ids.indexOf(this.id);
            -1 != id && my_cell_ids.splice(id, 1);
            this.destroyed = true;
            miniMapUnregisterToken(this.id);
        },
        setName: function(name) {
            this.name = name;
        },
        updatePos: function() {
            if (options.enableMultiCells || -1 != my_cell_ids.indexOf(this.id)) {
                if (! miniMapIsRegisteredToken(this.id))
                {
                    miniMapRegisterToken(
                        this.id,
                        miniMapCreateToken(this.id, this.color)
                    );
                }

                var size_n = this.nSize/7000;
                miniMapUpdateToken(this.id, (this.nx+7000)/14000 - size_n / 2, (this.ny+7000)/14000 - size_n / 2, size_n);
            }

            if (options.enablePosition && -1 != my_cell_ids.indexOf(this.id)) {
                miniMapUpdatePos(this.nx, this.ny);
            }
        }
    };

    // create a linked property from slave object
    // whenever master[prop] update, slave[prop] update
    function refer(master, slave, prop) {
        Object.defineProperty(master, prop, {
            get: function(){
                return slave[prop];
            },
            set: function(val) {
                slave[prop] = val;
            },
            enumerable: true,
            configurable: true
        });
    };

    // extract a websocket packet which contains the information of cells
    function extractCellPacket(data, offset) {
        var I = +new Date;
        var qa = false;
        var b = Math.random(), c = offset;
        var size = data.getUint16(c, true);
        c = c + 2;

        // destroy foods? (or cells?)
        for (var e = 0; e < size; ++e) {
            var p = cells[data.getUint32(c, true)],
                f = cells[data.getUint32(c + 4, true)],
                c = c + 8;
            p && f && (
                f.destroy(),
                f.ox = f.x,
                f.oy = f.y,
                f.oSize = f.size,
                f.nx = p.x,
                f.ny = p.y,
                f.nSize = f.size,
                f.updateTime = I)
        }

        // update or create cells (player)
        for (e = 0; ; ) {
            var d = data.getUint32(c, true);
            c += 4;
            if (0 == d) {
                break;
            }
            ++e;
            var p = data.getInt16(c, true),
                c = c + 2,
                f = data.getInt16(c, true),
                c = c + 2;
                g = data.getInt16(c, true);
                c = c + 2;
            for (var h = data.getUint8(c++), m = data.getUint8(c++), q = data.getUint8(c++), h = (h << 16 | m << 8 | q).toString(16); 6 > h.length; )
                h = "0" + h;

            var h = "#" + h,
                k = data.getUint8(c++),
                m = !!(k & 1),
                q = !!(k & 16);

            k & 2 && (c += 4);
            k & 4 && (c += 8);
            k & 8 && (c += 16);

            for (var n, k = ""; ; ) {
                n = data.getUint16(c, true);
                c += 2;
                if (0 == n)
                    break;
                k += String.fromCharCode(n)
            }

            n = k;
            k = null;

            // if d in cells then modify it, otherwise create a new cell
            cells.hasOwnProperty(d)
                ? (k = cells[d], k.updatePos(),
                   k.ox = k.x,
                   k.oy = k.y,
                   k.oSize = k.size,
                   k.color = h)
                : (k = new Cell(d, p, f, g, h, n),
                   k.pX = p,
                   k.pY = f);

            k.isVirus = m;
            k.isAgitated = q;
            k.nx = p;
            k.ny = f;
            k.nSize = g;
            k.updateCode = b;
            k.updateTime = I;
            n && k.setName(n);
        }

        // destroy cells(?)
        b = data.getUint32(c, true);
        c += 4;
        for (e = 0; e < b; e++)
            d = data.getUint32(c, true),
            c += 4, k = cells[d],
            null != k && k.destroy();
    }

    // extract the type of packet and dispatch it to a corresponding extractor
    function extractPacket(event) {
        var c = 0;
        var data = new DataView(event.data);
        240 == data.getUint8(c) && (c += 5);
        switch (data.getUint8(c++)) {
            case 16: // cells data
                extractCellPacket(data, c);
                break;
            case 20: // cleanup ids
                my_cell_ids = [];
                break;
            case 32: // cell id belongs me
                var id = data.getUint32(c, true);
                my_cell_ids.push(id);
                break;
        }
    };

    // the injected point, overwriting the WebSocket constructor
    window.WebSocket = function(url, protocols) {
        console.log('Listen');

    if (protocols === undefined) {
        protocols = [];
    }

        var ws = new _WebSocket(url, protocols);

        refer(this, ws, 'binaryType');
        refer(this, ws, 'bufferedAmount');
        refer(this, ws, 'extensions');
        refer(this, ws, 'protocol');
        refer(this, ws, 'readyState');
        refer(this, ws, 'url');

        this.send = function(data){
            return ws.send.call(ws, data);
        };

        this.close = function(code, reason){
            return ws.close.call(ws, code, reason);
        };

        this.onopen = function(event){};
        this.onclose = function(event){};
        this.onerror = function(event){};
        this.onmessage = function(event){};

        ws.onopen = function(event) {
            return this.onopen.call(ws, event);
        }.bind(this);

        ws.onmessage = function(event) {
            extractPacket(event);
            return this.onmessage.call(ws, event);
        }.bind(this);

        ws.onclose = function(event) {
            return this.onclose.call(ws, event);
        }.bind(this);

        ws.onerror = function(event) {
            return this.onerror.call(ws, event);
        }.bind(this);
    };

    window.WebSocket.prototype = _WebSocket;

    $(window.document).ready(function() {
        miniMapInit();
    })
})();
<commit_msg>Render the mini map on a canvas<commit_after>// ==UserScript==
// @name         agar-mini-map
// @namespace    http://github.com/dimotsai/
// @version      0.30
// @description  This script will show a mini map and your location on agar.io
// @author       dimotsai
// @license      MIT
// @match        http://agar.io/
// @grant        none
// @run-at       document-body
// ==/UserScript==

(function() {
    var _WebSocket = window.WebSocket;
    var $ = window.jQuery;

    var cells = [];
    var my_cell_ids = [];

    var options = {
        enableMultiCells: true,
        enablePosition: false
    };

    function miniMapCreateToken(id, color) {
        var mini_map_token = {
            id: id,
            color: color,
            x: 0,
            y: 0,
            size: 0
        };
        return mini_map_token;
    }

    function miniMapRegisterToken(id, token) {
        if (window.mini_map_tokens[id] === undefined) {
            // window.mini_map.append(token);
            window.mini_map_tokens[id] = token;
        }
    }

    function miniMapUnregisterToken(id) {
        if (window.mini_map_tokens[id] !== undefined) {
            // window.mini_map_tokens[id].detach();
            var x = window.mini_map_tokens[id].x;
            var y = window.mini_map_tokens[id].y;
            var size = window.mini_map_tokens[id].size;
            var canvas = window.mini_map;
            var ctx = canvas.getContext('2d');

            ctx.globalCompositeOperation = 'destination-out';
            ctx.beginPath();
            ctx.arc(x * canvas.width , y * canvas.height, size * canvas.width, 0 ,2 * Math.PI, false);
            ctx.closePath();
            ctx.fillStyle = window.mini_map_tokens[id].color;
            ctx.fill();
            delete window.mini_map_tokens[id];
        }
    }

    function miniMapIsRegisteredToken(id) {
        return window.mini_map_tokens[id] !== undefined;
    }

    function miniMapUpdateToken(id, x, y, size) {
        if (window.mini_map_tokens[id] !== undefined) {

            var draw = function(clear) {

                var canvas = window.mini_map;
                var ctx = canvas.getContext('2d');

                if (clear) {
                    ctx.globalCompositeOperation = 'destination-out';
                } else {
                    ctx.globalCompositeOperation = 'source-over';
                }
                ctx.lineWidth = 0;
                ctx.beginPath();
                ctx.arc(
                    window.mini_map_tokens[id].x * canvas.width,
                    window.mini_map_tokens[id].y * canvas.height,
                    window.mini_map_tokens[id].size * canvas.width,
                    0,
                    2 * Math.PI,
                    false
                );
                ctx.closePath();
                ctx.fillStyle = window.mini_map_tokens[id].color;
                ctx.fill();
            }

            draw(true);

            window.mini_map_tokens[id].x = x;
            window.mini_map_tokens[id].y = y;
            window.mini_map_tokens[id].size = size;

            draw(false);

            // window.mini_map_tokens[id]
            // .css({
            //     left:   x    * 100 + '%',
            //     top:    y    * 100 + '%',
            //     width:  size * 100 + '%',
            //     height: size * 100 + '%'
            // });

            return true;
        } else {
            return false;
        }
    }

    function miniMapUpdatePos(x, y) {
        window.mini_map_pos.text('x: ' + x.toFixed(0) + ', y: ' + y.toFixed(0));
    }

    function miniMapInit() {
        var $ = window.jQuery;
        window.mini_map_tokens = {};

        if ($('#mini-map-wrapper').length === 0) {
            var wrapper = $('<div>').attr('id', 'mini-map-wrapper').css({
                position: 'fixed',
                bottom: 10,
                right: 10,
                width: 300,
                height: 300,
                background: 'rgba(128, 128, 128, 0.58)'
            });

            var mini_map = $('<canvas>').attr({
                id: 'mini-map',
                width: 300,
                height: 300
            }).css({
                width: '100%',
                height: '100%',
                position: 'relative'
            });

            wrapper.append(mini_map).appendTo(document.body);

            window.mini_map = mini_map[0];
        }

        if ($('#mini-map-pos').length === 0) {
            window.mini_map_pos = $('<div>').attr('id', 'mini-map-pos').css({
                bottom: 10,
                right: 10,
                color: 'white',
                fontSize: 15,
                fontWeight: 800,
                position: 'fixed',
                padding: '0px 10px'
            }).appendTo(document.body);
        }

    }

    // cell constructor
    function Cell(id, x, y, size, color, name) {
        cells[id] = this;
        this.id = id;
        this.ox = this.x = x;
        this.oy = this.y = y;
        this.oSize = this.size = size;
        this.color = color;
        this.points = [];
        this.pointsAcc = [];
        this.setName(name);
    }

    Cell.prototype = {
        id: 0,
        points: null,
        pointsAcc: null,
        name: null,
        nameCache: null,
        sizeCache: null,
        x: 0,
        y: 0,
        size: 0,
        ox: 0,
        oy: 0,
        oSize: 0,
        nx: 0,
        ny: 0,
        nSize: 0,
        updateTime: 0,
        updateCode: 0,
        drawTime: 0,
        destroyed: false,
        isVirus: false,
        isAgitated: false,
        wasSimpleDrawing: true,

        destroy: function() {
            delete cells[this.id];
            id = my_cell_ids.indexOf(this.id);
            -1 != id && my_cell_ids.splice(id, 1);
            this.destroyed = true;
            miniMapUnregisterToken(this.id);
        },
        setName: function(name) {
            this.name = name;
        },
        updatePos: function() {
            if (options.enableMultiCells || -1 != my_cell_ids.indexOf(this.id)) {
                if (! miniMapIsRegisteredToken(this.id))
                {
                    miniMapRegisterToken(
                        this.id,
                        miniMapCreateToken(this.id, this.color)
                    );
                }

                var size_n = this.nSize/7000;
                miniMapUpdateToken(this.id, (this.nx+7000)/14000 - size_n / 2, (this.ny+7000)/14000 - size_n / 2, size_n);
            }

            if (options.enablePosition && -1 != my_cell_ids.indexOf(this.id)) {
                miniMapUpdatePos(this.nx, this.ny);
            }
        }
    };

    // create a linked property from slave object
    // whenever master[prop] update, slave[prop] update
    function refer(master, slave, prop) {
        Object.defineProperty(master, prop, {
            get: function(){
                return slave[prop];
            },
            set: function(val) {
                slave[prop] = val;
            },
            enumerable: true,
            configurable: true
        });
    };

    // extract a websocket packet which contains the information of cells
    function extractCellPacket(data, offset) {
        var I = +new Date;
        var qa = false;
        var b = Math.random(), c = offset;
        var size = data.getUint16(c, true);
        c = c + 2;

        // destroy foods? (or cells?)
        for (var e = 0; e < size; ++e) {
            var p = cells[data.getUint32(c, true)],
                f = cells[data.getUint32(c + 4, true)],
                c = c + 8;
            p && f && (
                f.destroy(),
                f.ox = f.x,
                f.oy = f.y,
                f.oSize = f.size,
                f.nx = p.x,
                f.ny = p.y,
                f.nSize = f.size,
                f.updateTime = I)
        }

        // update or create cells (player)
        for (e = 0; ; ) {
            var d = data.getUint32(c, true);
            c += 4;
            if (0 == d) {
                break;
            }
            ++e;
            var p = data.getInt16(c, true),
                c = c + 2,
                f = data.getInt16(c, true),
                c = c + 2;
                g = data.getInt16(c, true);
                c = c + 2;
            for (var h = data.getUint8(c++), m = data.getUint8(c++), q = data.getUint8(c++), h = (h << 16 | m << 8 | q).toString(16); 6 > h.length; )
                h = "0" + h;

            var h = "#" + h,
                k = data.getUint8(c++),
                m = !!(k & 1),
                q = !!(k & 16);

            k & 2 && (c += 4);
            k & 4 && (c += 8);
            k & 8 && (c += 16);

            for (var n, k = ""; ; ) {
                n = data.getUint16(c, true);
                c += 2;
                if (0 == n)
                    break;
                k += String.fromCharCode(n)
            }

            n = k;
            k = null;

            // if d in cells then modify it, otherwise create a new cell
            cells.hasOwnProperty(d)
                ? (k = cells[d], k.updatePos(),
                   k.ox = k.x,
                   k.oy = k.y,
                   k.oSize = k.size,
                   k.color = h)
                : (k = new Cell(d, p, f, g, h, n),
                   k.pX = p,
                   k.pY = f);

            k.isVirus = m;
            k.isAgitated = q;
            k.nx = p;
            k.ny = f;
            k.nSize = g;
            k.updateCode = b;
            k.updateTime = I;
            n && k.setName(n);
        }

        // destroy cells(?)
        b = data.getUint32(c, true);
        c += 4;
        for (e = 0; e < b; e++)
            d = data.getUint32(c, true),
            c += 4, k = cells[d],
            null != k && k.destroy();
    }

    // extract the type of packet and dispatch it to a corresponding extractor
    function extractPacket(event) {
        var c = 0;
        var data = new DataView(event.data);
        240 == data.getUint8(c) && (c += 5);
        switch (data.getUint8(c++)) {
            case 16: // cells data
                extractCellPacket(data, c);
                break;
            case 20: // cleanup ids
                my_cell_ids = [];
                break;
            case 32: // cell id belongs me
                var id = data.getUint32(c, true);
                my_cell_ids.push(id);
                break;
        }
    };

    // the injected point, overwriting the WebSocket constructor
    window.WebSocket = function(url, protocols) {
        console.log('Listen');

    if (protocols === undefined) {
        protocols = [];
    }

        var ws = new _WebSocket(url, protocols);

        refer(this, ws, 'binaryType');
        refer(this, ws, 'bufferedAmount');
        refer(this, ws, 'extensions');
        refer(this, ws, 'protocol');
        refer(this, ws, 'readyState');
        refer(this, ws, 'url');

        this.send = function(data){
            return ws.send.call(ws, data);
        };

        this.close = function(code, reason){
            return ws.close.call(ws, code, reason);
        };

        this.onopen = function(event){};
        this.onclose = function(event){};
        this.onerror = function(event){};
        this.onmessage = function(event){};

        ws.onopen = function(event) {
            return this.onopen.call(ws, event);
        }.bind(this);

        ws.onmessage = function(event) {
            extractPacket(event);
            return this.onmessage.call(ws, event);
        }.bind(this);

        ws.onclose = function(event) {
            return this.onclose.call(ws, event);
        }.bind(this);

        ws.onerror = function(event) {
            return this.onerror.call(ws, event);
        }.bind(this);
    };

    window.WebSocket.prototype = _WebSocket;

    $(window.document).ready(function() {
        miniMapInit();
    })
})();
<|endoftext|>


<commit_before>/* global $, google, nv, d3, moment, console, Messaging */
'use strict';

// Wait until document load.
$(function() {
  // Initialise Globals
  // ==================

  // Google Maps
  // -----------
  // Google Maps object, centred on York.
  var map = new google.maps.Map($('#mapCanvas').get(0), {
    center: new google.maps.LatLng(53.958333, -1.080278),
    zoom: 11
  });

  // Trace Colors
  // ------------
  // Function that returns next color from cyclical queue.
  var nextColor = (function() {
    var colorQueue = ['#E84A21', '#5BFF75', '#FFF84E', '#E80C7A', '#1146FF'];
    return function() {
      var temp = colorQueue.shift();
      colorQueue.push(temp);
      return temp;
    };
  })();

  // MQTT Client
  // -----------

  // Generate a five character client ID suffix.
  var clientID = (function() {
    var id = 'viewer-';
    for(var i = 0; i < 10; i++) {
      id += Math.floor(Math.random() * 16).toString(16);
    }
    return id;
  })();

  // Dictionary for message handlers.
  var handlerDictionary = {};

  // Initialise a conneciton to the MessageSight broker.
  var mqttClient = new Messaging.Client('5.153.17.246', 13531, clientID);
  console.log('Connecting with client ID: ' + clientID);
  mqttClient.connect({
    onSuccess: function() {
      console.log('MQTT Client Connected');
      mqttClient.subscribe('/test/debug');
    }
  });

  // On message arrival, lookup trace in dictionary for handler.
  mqttClient.onMessageArrived = function(msg) {
    var dest = msg.destinationName;
    if (dest.indexOf('/trace/') === 0) {
      var traceID = dest.substr('/trace/'.length);
      if (traceID in handlerDictionary) {
        handlerDictionary[traceID](JSON.parse(msg.payloadString));
      } else {
        console.log('Unknown traceID: ' + traceID);
      }
    } else {
      console.log('Unknown topic:');
      console.log(msg);
    }
  };

  // Send command (requires connection)
  var sendCommand = function (obj) {
    var message = new Messaging.Message(JSON.stringify(obj));
    message.destinationName = "/command/";
    message.qos = 1;
    mqttClient.send(message);
  };

  // Add trace
  // ---------

  // Add Trace form validity
  var isAddTraceFormValid = function() {
    // Page 2: Can only view in full if loading from disk.
    if ($('#telemetryViewRadioPartial').prop('checked')) {
      if ($('#telemetrySourceRadioLocal').prop('checked')) {
        return 'Cannot replay trace without first uploading to server.';
      }
    }

    // Page 3: Tick and output MQTT on replay
    var isReplaying =
      $('#telemetrySourceRadioRemote').prop('checked') &&
      $('#telemetryViewRadioPartial').prop('checked');
    $('#telemetryTargetMQTT').prop('checked', isReplaying);
    $('#mqttTarget').val(isReplaying ? $('#remoteID').val() : "");

    // Page 1: Are boxes filled?
    if ($('#telemetrySourceRadioLocal').prop('checked')) {
      if ($('#uploadFile').get(0).files.length !== 1) {
        return 'No source file selected.';
      }
    } else if ($('#telemetrySourceRadioRemote').prop('checked')) {
      if (! $('#remoteID').val()) {
        return 'No CouchDB source ID entered.';
      }
    } else if ($('#telemetrySourceRadioMQTT').prop('checked')) {
      if (! $('#mqttSource').val()) {
        return 'No MQTT source topic entered.';
      }
    }

    // Page 2: Are boxes filled?
    /* if ($('#telemetryViewRadioPartial').prop('checked')) {
      if (! $('#startTimePartial').val()) {
        return 'No start time entered.';
      }
    } */

    // Page 3: Are boxes filled?
    if ($('#telemetryTargetCouchDB').prop('checked')) {
      if (! $('#storeID').val()) {
        return 'No couch DB storage ID entered.';
      }
    } else if ($('#telemetryTargetMQTT').prop('checked')) {
      if (! $('#mqttTarget').val()) {
        return 'No MQTT topic topic entered.';
      }
    }
  };

  $('#addAccordian').change(function() {
    var isValid = isAddTraceFormValid();
    if (isValid) {
      $('#validationError').text(isValid);
    } else {
      $('#validationError').text("");
    }
  });

  var doSource = function() {
    if ($('#telemetrySourceRadioLocal').prop('checked')) {
      // Load the file from disk
      var reader = new FileReader();
      reader.onload = function(event) {
        doTarget(parseGPX(event.target.result), null);
      }
      reader.readAsText($('#uploadFile').get(0).files[0]);
    } else if ($('#telemetrySourceRadioRemote').prop('checked')) {
      var topicSegment = $('#remoteID').val();
      if ($('#telemetryViewRadioFull').prop('checked')) {
        $.ajax({
          url: '/db/traces/_design/TraceDesign/_view/getTraceByTopic?key='
               + encodeURIComponent('"' + topicSegment + '"'),
          type: 'GET',
          contentType: 'application/json',
          dataType: 'json',
          success: function(traceData) {
            if (traceData.total_rows == 1) {
              $.ajax({
                url: '/db/points/_design/PointsDesign/_view/getPointsByTopic?key='
                     + encodeURIComponent('"' + topicSegment + '"'),
                type: 'GET',
                contentType: 'application/json',
                dataType: 'json',
                success: function(pointsData) {
                  var history = {
                    title: traceData.rows[0].value.title,
                    startTst: traceData.rows[0].value.startTst,
                    endTst: traceData.rows[0].value.endTst,
                    points: []
                  };

                  pointsData.rows.forEach( function(item) {
                    history.points.push(item.value);
                  });

                  doTarget(history, null);
                }
              });
            };
          }
        });
      } else if ($('#telemetryViewRadioPartial').prop('checked')) {
        var history = {
          title: '/replay/' + topicSegment,
          points: []
        };
        sendCommand({command: 'load', topic: topicSegment});
        sendCommand({command: 'start', topic: topicSegment});
        doTarget(history, topicSegment);
      }
    } else if ($('#telemetrySourceRadioMQTT').prop('checked')) {
      var mqttTopic = $('#mqttSource').val();
      var history = {
        title: mqttTopic,
        startDate: moment().format('X'),
        points: []
      };
      doTarget(history, mqttTopic);
    }
  };

  var doTarget = function(history, mqttTopic) {
    if ($('#telemetryTargetCouchDB').prop('checked')) {
      var traceID = $('#storeID').val();
      var points = [];
      history.points.forEach(function(item) {
        item._id = traceID + item.tst;
        item.topic = traceID;
        points.push(item);
      });

      console.log('POSTing bulk points data.');
      $.ajax({
        url: '/db/points/_bulk_docs',
        type: 'POST',
        data: JSON.stringify({docs: points}),
        contentType: 'application/json',
        dataType: 'json',
        complete: function(jqXHR, textStatus) {
          console.log('Points: ');
          console.log(textStatus);
        }
      });

      $.ajax({
        url: '/db/traces',
        type: 'POST',
        data: JSON.stringify({
          startTst:  history.startDate,
          endTst:  history.endDate,
          title:  history.title,
          topic: traceID
        }),
        contentType: 'application/json',
        dataType: 'json',
        complete: function(jqXHR, textStatus) {
          console.log('Trace: ');
          console.log(textStatus);
        }
      });
    }

    if ($('#telemetryTargetMQTT').prop('checked')) {
      mqttTopic = '/trace/' + $('#mqttTarget').val();
    }

    if ($('#telemetryTargetMap').prop('checked')) {
      var trace = plotArchive(history);
      if (mqttTopic) {
        trace.subscribeFeed(mqttTopic);
      } else {
        trace.finishFeed();
      }
    }

    // Finally, hide dialog.
    $('#addTrace').modal('hide');
  };

  // Save button on add trace.
  $('#addTraceSave').click(function() {
    if (!isAddTraceFormValid()) {
      doSource();
    }
  });

  // Speed vs. Time Chart
  // --------------------

  // Chart object.
  var chart;

  // Chart data object.
  var chartData = [];

  // Create chart.
  nv.addGraph(function() {
    chart = nv.models.lineChart()
      .options({
        x: function(d,i) { return i; },
        showXAxis: true,
        showYAxis: true,
        showLegend: false,
        transitionDuration: 250,
        useInteractiveGuideline: true
      });

    chart.xAxis
      .axisLabel('Minutes')
      .tickFormat(d3.format('d'));

    chart.yAxis
      .axisLabel('Speed (km/h)')
      .axisLabelDistance(30)
      .tickFormat(d3.format('.02f'));

    d3.select('#chart_div svg')
      .datum(chartData)
      .call(chart);

    nv.utils.windowResize(chart.update);

    return chart;
  });

  // Dragable Panel
  // --------------
  // Use JQuery UI to drag panel
  $('#controls').draggable({
    handle: '.panel-heading'
  });

  // Plotting traces
  // ===============
  // Bounds for map data.
  var mapBounds = new google.maps.LatLngBounds();

  // Trace Class
  // -----------
  var Trace = function() {
    var self = this;

    // Color representing this trace.
    var color = nextColor();

    // Table row for this trace.
    var symbolCell = $('').html('●')
                   .css('color', color);
    var nameCell = $('Loading...	');
    var startCell = $('');
    var totalCell = $('Live	');
    $('#traceListing').append(
      $('
').append(symbolCell, nameCell, startCell, totalCell)
    );

    // Polyline for Google Maps.
    self.polyLine = new google.maps.Polyline({
      strokeColor: color,
      strokeOpacity: 0.7,
      strokeWeight: 4,
      map: map
    });

    // Marker for Google Maps.
    var iconUrl = 'http://chart.apis.google.com/chart?chst=d_map_pin_letter&chld=%E2%80%A2|' + color.substr(1);
    self.marker = new google.maps.Marker({
      title: 'Loading',
      icon: iconUrl,
      map: map
    });

    // Chart data for nvd3.
    self.seriesData = {
      values: [],
      key: 'Loading...',
      color: color
    };
    chartData.push(self.seriesData);

    // Change title of the trace.
    self.setTitle = function(title) {
      nameCell.text(title);
      self.seriesData.key = title;
      self.marker.setTitle(title);
    };

    // Add datapoint.
    var lastMinute = null;
    self.addDataPoint = function(time, point) {
      // Add to polyline
      var path = self.polyLine.getPath();
      path.push(point);
      self.polyLine.setPath(path);

      // Move marker
      self.marker.setPosition(point);

      // If initial datapoint, set start-time
      if (path.length === 1) {
        self.startTime = moment(time);
        startCell.text(self.startTime.calendar());
      }

      // Extend map bounds
      mapBounds.extend(point);
      map.fitBounds(mapBounds);

      // Add to Speed vs. Time graph.
      if (lastMinute) {
        if (1 <= moment(time).diff(lastMinute.time, 'minutes', true)) {
          var timeDelta = moment(time).diff(lastMinute.time, 'hours', true);
          var distDelta = google.maps.geometry.spherical
                        .computeDistanceBetween(lastMinute.point, point) / 1000;
          var speed = distDelta / timeDelta;

          self.seriesData.values.push({
            x: moment(lastMinute.time).diff(self.startTime, 'minutes', true),
            y: speed
          });
          lastMinute = {time: time, point: point};
        }
      } else {
        lastMinute = {time: time, point: point};
      }
    };

    // Subscribe to live feed
    self.subscribeFeed = function(traceID) {
      handlerDictionary[traceID] = function(obj) {
        self.addDataPoint(
          moment.unix(obj.tst),
          new google.maps.LatLng(obj.lat, obj.lon)
        );
        chart.update();
      };
      mqttClient.subscribe('/trace/' + traceID);
    };

    // Finish a live feed
    self.finishFeed = function() {
      var delta = moment(lastMinute.time).diff(self.startTime, 'hours', true).toFixed(2);
      nameCell.removeClass('text-success');
      totalCell.text(delta + ' hours').removeClass('text-success');
    };
  };

  // Parse a GPX file
  var parseGPX = function(txt) {
    var $xml = $(txt);
    var $trkpts = $xml.children('trk')
                .children('trkseg').children('trkpt');

    var startDate, endDate;
    var points = [];

    $trkpts.each(function() {
        var lat = $(this).attr('lat');
        var lon = $(this).attr('lon');
        var tst = moment($(this).find('time').text()).format('X');

        if (!startDate) startDate = tst;
        endDate = tst;
        points.push({tst: tst, lat: lat, lon: lon});
    });

    return {
      title: $xml.children('trk').children('name').text(),
      startDate: startDate,
      endDate: endDate,
      points: points
    };
  };

  // Function for plotting new traces from archive telemetry data
  var plotArchive = function(data) {
    var trace = new Trace();

    data.points.forEach(function(item) {
      var point = new google.maps.LatLng(item.lat, item.lon);
      trace.addDataPoint(moment.unix(item.tst), point);
    });

    trace.setTitle(data.title);
    chart.update();
    console.log('Done.');

    return trace;
  };
});<commit_msg>Much functionality<commit_after>/* global $, google, nv, d3, moment, console, Messaging */
'use strict';

// Wait until document load.
$(function() {
  // Initialise Globals
  // ==================

  // Google Maps
  // -----------
  // Google Maps object, centred on York.
  var map = new google.maps.Map($('#mapCanvas').get(0), {
    center: new google.maps.LatLng(53.958333, -1.080278),
    zoom: 11
  });

  // Trace Colors
  // ------------
  // Function that returns next color from cyclical queue.
  var nextColor = (function() {
    var colorQueue = ['#E84A21', '#5BFF75', '#FFF84E', '#E80C7A', '#1146FF'];
    return function() {
      var temp = colorQueue.shift();
      colorQueue.push(temp);
      return temp;
    };
  })();

  // MQTT Client
  // -----------

  // Generate a five character client ID suffix.
  var clientID = (function() {
    var id = 'viewer-';
    for(var i = 0; i < 10; i++) {
      id += Math.floor(Math.random() * 16).toString(16);
    }
    return id;
  })();

  // Dictionary for message handlers.
  var handlerDictionary = {};

  // Initialise a conneciton to the MessageSight broker.
  var mqttClient = new Messaging.Client('5.153.17.246', 13531, clientID);
  console.log('Connecting with client ID: ' + clientID);
  mqttClient.connect({
    onSuccess: function() {
      console.log('MQTT Client Connected');
      mqttClient.subscribe('/test/debug');
    }
  });

  // On message arrival, lookup trace in dictionary for handler.
  mqttClient.onMessageArrived = function(msg) {
    var dest = msg.destinationName;
    if (dest.indexOf('/trace/') === 0) {
      var traceID = dest.substr('/trace/'.length);
      if (traceID in handlerDictionary) {
        handlerDictionary[traceID](JSON.parse(msg.payloadString));
      } else {
        console.log('Unknown traceID: ' + traceID);
      }
    } else {
      console.log('Unknown topic:');
      console.log(msg);
    }
  };

  // Send command (requires connection)
  var sendCommand = function (obj) {
    var message = new Messaging.Message(JSON.stringify(obj));
    message.destinationName = "/command/";
    message.qos = 1;
    mqttClient.send(message);
  };

  // Add trace
  // ---------

  // Add Trace form validity
  var isAddTraceFormValid = function() {
    // Page 2: Can only view in full if loading from disk.
    if ($('#telemetryViewRadioPartial').prop('checked')) {
      if ($('#telemetrySourceRadioLocal').prop('checked')) {
        return 'Cannot replay trace without first uploading to server.';
      }
    }

    // Page 3: Tick and output MQTT on replay
    var isReplaying =
      $('#telemetrySourceRadioRemote').prop('checked') &&
      $('#telemetryViewRadioPartial').prop('checked');
    $('#telemetryTargetMQTT').prop('checked', isReplaying);
    $('#mqttTarget').val(isReplaying ? $('#remoteID').val() : "");

    // Page 1: Are boxes filled?
    if ($('#telemetrySourceRadioLocal').prop('checked')) {
      if ($('#uploadFile').get(0).files.length !== 1) {
        return 'No source file selected.';
      }
    } else if ($('#telemetrySourceRadioRemote').prop('checked')) {
      if (! $('#remoteID').val()) {
        return 'No CouchDB source ID entered.';
      }
    } else if ($('#telemetrySourceRadioMQTT').prop('checked')) {
      if (! $('#mqttSource').val()) {
        return 'No MQTT source topic entered.';
      }
    }

    // Page 2: Are boxes filled?
    /* if ($('#telemetryViewRadioPartial').prop('checked')) {
      if (! $('#startTimePartial').val()) {
        return 'No start time entered.';
      }
    } */

    // Page 3: Are boxes filled?
    if ($('#telemetryTargetCouchDB').prop('checked')) {
      if (! $('#storeID').val()) {
        return 'No couch DB storage ID entered.';
      }
    } else if ($('#telemetryTargetMQTT').prop('checked')) {
      if (! $('#mqttTarget').val()) {
        return 'No MQTT topic topic entered.';
      }
    }
  };

  $('#addAccordian').change(function() {
    var isValid = isAddTraceFormValid();
    if (isValid) {
      $('#validationError').text(isValid);
    } else {
      $('#validationError').text("");
    }
  });

  var doSource = function() {
    if ($('#telemetrySourceRadioLocal').prop('checked')) {
      // Load the file from disk
      var reader = new FileReader();
      reader.onload = function(event) {
        doTarget(parseGPX(event.target.result), null);
      }
      reader.readAsText($('#uploadFile').get(0).files[0]);
    } else if ($('#telemetrySourceRadioRemote').prop('checked')) {
      var topicSegment = $('#remoteID').val();
      if ($('#telemetryViewRadioFull').prop('checked')) {
        $.ajax({
          url: '/db/traces/_design/TraceDesign/_view/getTraceByTopic?key='
               + encodeURIComponent('"' + topicSegment + '"'),
          type: 'GET',
          contentType: 'application/json',
          dataType: 'json',
          success: function(traceData) {
            if (traceData.total_rows == 1) {
              $.ajax({
                url: '/db/points/_design/PointsDesign/_view/getPointsByTopic?key='
                     + encodeURIComponent('"' + topicSegment + '"'),
                type: 'GET',
                contentType: 'application/json',
                dataType: 'json',
                success: function(pointsData) {
                  var history = {
                    title: traceData.rows[0].value.title,
                    startTst: traceData.rows[0].value.startTst,
                    endTst: traceData.rows[0].value.endTst,
                    points: []
                  };

                  pointsData.rows.forEach( function(item) {
                    history.points.push(item.value);
                  });

                  doTarget(history, null);
                }
              });
            };
          }
        });
      } else if ($('#telemetryViewRadioPartial').prop('checked')) {
        var history = {
          title: '/replay/' + topicSegment,
          points: []
        };
        sendCommand({command: 'load', topic: topicSegment});
        sendCommand({command: 'start', topic: topicSegment});
        doTarget(history, topicSegment);
      }
    } else if ($('#telemetrySourceRadioMQTT').prop('checked')) {
      var mqttTopic = $('#mqttSource').val();
      var history = {
        title: mqttTopic,
        startDate: moment().format('X'),
        points: []
      };
      doTarget(history, mqttTopic);
    }
  };

  var doTarget = function(history, mqttTopic) {
    if ($('#telemetryTargetCouchDB').prop('checked')) {
      var traceID = $('#storeID').val();
      var points = [];
      history.points.forEach(function(item) {
        item._id = traceID + item.tst;
        item.topic = traceID;
        points.push(item);
      });

      console.log('POSTing bulk points data.');
      $.ajax({
        url: '/db/points/_bulk_docs',
        type: 'POST',
        data: JSON.stringify({docs: points}),
        contentType: 'application/json',
        dataType: 'json',
        complete: function(jqXHR, textStatus) {
          console.log('Points: ');
          console.log(textStatus);
        }
      });

      $.ajax({
        url: '/db/traces',
        type: 'POST',
        data: JSON.stringify({
          startTst:  history.startDate,
          endTst:  history.endDate,
          title:  history.title,
          topic: traceID
        }),
        contentType: 'application/json',
        dataType: 'json',
        complete: function(jqXHR, textStatus) {
          console.log('Trace: ');
          console.log(textStatus);
        }
      });
    }

    if ($('#telemetryTargetMap').prop('checked')) {
      var trace = plotArchive(history);
      if (mqttTopic) {
        console.log('Subscribing to ' + mqttTopic);
        trace.subscribeFeed(mqttTopic);
      } else {
        trace.finishFeed();
      }
    }

    // Finally, hide dialog.
    $('#addTrace').modal('hide');
  };

  // Save button on add trace.
  $('#addTraceSave').click(function() {
    if (!isAddTraceFormValid()) {
      doSource();
    }
  });

  // Speed vs. Time Chart
  // --------------------

  // Chart object.
  var chart;

  // Chart data object.
  var chartData = [];

  // Create chart.
  nv.addGraph(function() {
    chart = nv.models.lineChart()
      .options({
        x: function(d,i) { return i; },
        showXAxis: true,
        showYAxis: true,
        showLegend: false,
        transitionDuration: 250,
        useInteractiveGuideline: true
      });

    chart.xAxis
      .axisLabel('Minutes')
      .tickFormat(d3.format('d'));

    chart.yAxis
      .axisLabel('Speed (km/h)')
      .axisLabelDistance(30)
      .tickFormat(d3.format('.02f'));

    d3.select('#chart_div svg')
      .datum(chartData)
      .call(chart);

    nv.utils.windowResize(chart.update);

    return chart;
  });

  // Dragable Panel
  // --------------
  // Use JQuery UI to drag panel
  $('#controls').draggable({
    handle: '.panel-heading'
  });

  // Plotting traces
  // ===============
  // Bounds for map data.
  var mapBounds = new google.maps.LatLngBounds();

  // Trace Class
  // -----------
  var Trace = function() {
    var self = this;

    // Color representing this trace.
    var color = nextColor();

    // Table row for this trace.
    var symbolCell = $('').html('●')
                   .css('color', color);
    var nameCell = $('Loading...	');
    var startCell = $('');
    var totalCell = $('	');
    var playPause = $('<button type="button" class="btn btn-primary btn-xs"> Pause</button>');
    totalCell.append(playPause);
    $('#traceListing').append(
      $('
').append(symbolCell, nameCell, startCell, totalCell)
    );

    // Polyline for Google Maps.
    self.polyLine = new google.maps.Polyline({
      strokeColor: color,
      strokeOpacity: 0.7,
      strokeWeight: 4,
      map: map
    });

    // Marker for Google Maps.
    var iconUrl = 'http://chart.apis.google.com/chart?chst=d_map_pin_letter&chld=%E2%80%A2|' + color.substr(1);
    self.marker = new google.maps.Marker({
      title: 'Loading',
      icon: iconUrl,
      map: map
    });

    // Chart data for nvd3.
    self.seriesData = {
      values: [],
      key: 'Loading...',
      color: color
    };
    chartData.push(self.seriesData);

    // Change title of the trace.
    self.setTitle = function(title) {
      nameCell.text(title);
      self.seriesData.key = title;
      self.marker.setTitle(title);
    };

    // Add datapoint.
    var lastMinute = null;
    self.addDataPoint = function(time, point) {
      // Add to polyline
      var path = self.polyLine.getPath();
      path.push(point);
      self.polyLine.setPath(path);

      // Move marker
      self.marker.setPosition(point);

      // If initial datapoint, set start-time
      if (path.length === 1) {
        self.startTime = moment(time);
        startCell.text(self.startTime.calendar());
      }

      // Extend map bounds
      mapBounds.extend(point);
      map.fitBounds(mapBounds);

      // Add to Speed vs. Time graph.
      if (lastMinute) {
        if (1 <= moment(time).diff(lastMinute.time, 'minutes', true)) {
          var timeDelta = moment(time).diff(lastMinute.time, 'hours', true);
          var distDelta = google.maps.geometry.spherical
                        .computeDistanceBetween(lastMinute.point, point) / 1000;
          var speed = distDelta / timeDelta;

          self.seriesData.values.push({
            x: moment(lastMinute.time).diff(self.startTime, 'minutes', true),
            y: speed
          });
          lastMinute = {time: time, point: point};
        }
      } else {
        lastMinute = {time: time, point: point};
      }
    };

    // Subscribe to live feed
    var traceID;
    self.subscribeFeed = function(_traceID) {
      traceID = _traceID;
      handlerDictionary[traceID] = function(obj) {
        self.addDataPoint(
          moment.unix(obj.tst),
          new google.maps.LatLng(obj.lat, obj.lon)
        );
        chart.update();
      };
      mqttClient.subscribe('/trace/' + traceID);
    };

    // Finish a live feed
    self.finishFeed = function() {
      var delta = moment(lastMinute.time).diff(self.startTime, 'hours', true).toFixed(2);
      nameCell.removeClass('text-success');
      totalCell.text(delta + ' hours').removeClass('text-success');
    };

    // Toggle a replay
    var isRunning = true;
    self.toggleReplay = function() {
      isRunning = !isRunning;
      sendCommand({'command': isRunning ? 'start' : 'pause', 'topic': traceID});
      if (!isRunning) {
        playPause.html(' Play');
      } else {
        playPause.html(' Pause');
      }
      return isRunning;
    };
    playPause.click(self.toggleReplay);
  };

  // Parse a GPX file
  var parseGPX = function(txt) {
    var $xml = $(txt);
    var $trkpts = $xml.children('trk')
                .children('trkseg').children('trkpt');

    var startDate, endDate;
    var points = [];

    $trkpts.each(function() {
        var lat = $(this).attr('lat');
        var lon = $(this).attr('lon');
        var tst = moment($(this).find('time').text()).format('X');

        if (!startDate) startDate = tst;
        endDate = tst;
        points.push({tst: tst, lat: lat, lon: lon});
    });

    return {
      title: $xml.children('trk').children('name').text(),
      startDate: startDate,
      endDate: endDate,
      points: points
    };
  };

  // Function for plotting new traces from archive telemetry data
  var plotArchive = function(data) {
    var trace = new Trace();

    data.points.forEach(function(item) {
      var point = new google.maps.LatLng(item.lat, item.lon);
      trace.addDataPoint(moment.unix(item.tst), point);
    });

    trace.setTitle(data.title);
    chart.update();
    console.log('Done.');

    return trace;
  };
});<|endoftext|>


<commit_before>/* global $, google, nv, d3, moment, console, Messaging */
'use strict';

// Wait until document load.
$(function() {
  // Initialise Globals
  // ==================

  // Google Maps
  // -----------
  // Google Maps object, centred on York.
  var map = new google.maps.Map($('#mapCanvas').get(0), {
    center: new google.maps.LatLng(53.958333, -1.080278),
    zoom: 11
  });

  // Trace Colors
  // ------------
  // Function that returns next color from cyclical queue.
  var nextColor = (function() {
    var colorQueue = ['#E84A21', '#5BFF75', '#FFF84E', '#E80C7A', '#1146FF'];
    return function() {
      var temp = colorQueue.shift();
      colorQueue.push(temp);
      return temp;
    };
  })();

  // Add trace
  // ---------

  // Add Trace form validity
  var isAddTraceFormValid = function() {
    // Page 2: Can only view in full if loading from disk.
    if ($('#telemetryViewRadioPartial').prop('checked')) {
      if ($('#telemetrySourceRadioLocal').prop('checked')) {
        return 'Cannot replay trace without first uploading to server.';
      }
    }

    // Page 1: Are boxes filled?
    if ($('#telemetrySourceRadioLocal').prop('checked')) {
      if ($('#uploadFile').get(0).files.length !== 1) {
        return 'No source file selected.';
      }
    } else if ($('#telemetrySourceRadioRemote').prop('checked')) {
      if (! $('#remoteID').val()) {
        return 'No CouchDB source ID entered.';
      }
    } else if ($('#telemetrySourceRadioMQTT').prop('checked')) {
      if (! $('#mqttSource').val()) {
        return 'No MQTT source topic entered.';
      }
    }

    // Page 2: Are boxes filled?
    if ($('#telemetryViewRadioPartial').prop('checked')) {
      if (! $('#startTimePartial').val()) {
        return 'No start time entered.';
      }
    }

    // Page 3: Are boxes filled?
    if ($('#telemetryTargetCouchDB').prop('checked')) {
      if (! $('#storeID').val()) {
        return 'No couch DB storage ID entered.';
      }
    } else if ($('#telemetryTargetMQTT').prop('checked')) {
      if (! $('#mqttTarget').val()) {
        return 'No MQTT topic topic entered.';
      }
    }
  };

  $('#addAccordian').change(function() {
    var isValid = isAddTraceFormValid();
    if (isValid) {
      $('#validationError').text(isValid);
    } else {
      $('#validationError').text("");
    }
  });

  var doSource = function() {
    if ($('#telemetrySourceRadioLocal').prop('checked')) {
      // Load the file from disk
      var reader = new FileReader();
      reader.onload = function(event) {
        doTarget(parseGPX(event.target.result), null);
      }
      reader.readAsText($('#uploadFile').get(0).files[0]);
    } else if ($('#telemetrySourceRadioRemote').prop('checked')) {
      alert('Not yet completed');
    } else if ($('#telemetrySourceRadioMQTT').prop('checked')) {
      var mqttTopic = $('#mqttSource').val();
      var history = {
        title: mqttTopic,
        startDate: moment().format('X'),
        points: []
      };
      doTarget(history, mqttTopic);
    }
  };

  var doTarget = function(history, mqttTopic) {
    if ($('#telemetryTargetCouchDB').prop('checked')) {
      alert('Not yet completed');
    }

    if ($('#telemetryTargetMQTT').prop('checked')) {
      mqttTopic = '/trace/' + $('#mqttTarget').val();
    }

    if ($('#telemetryTargetMap').prop('checked')) {
      var trace = plotArchive(history);
      if (mqttTopic) {
        trace.subscribeFeed(mqttTopic);
      } else {
        trace.finishFeed();
      }
    }

    // Finally, hide dialog.
    $('#addTrace').modal('hide');
  };

  // Save button on add trace.
  $('#addTraceSave').click(function() {
    if (!isAddTraceFormValid()) {
      doSource();
    }
  });

  // Speed vs. Time Chart
  // --------------------

  // Chart object.
  var chart;

  // Chart data object.
  var chartData = [];

  // Create chart.
  nv.addGraph(function() {
    chart = nv.models.lineChart()
      .options({
        x: function(d,i) { return i; },
        showXAxis: true,
        showYAxis: true,
        showLegend: false,
        transitionDuration: 250,
        useInteractiveGuideline: true
      });

    chart.xAxis
      .axisLabel('Minutes')
      .tickFormat(d3.format('d'));

    chart.yAxis
      .axisLabel('Speed (km/h)')
      .axisLabelDistance(30)
      .tickFormat(d3.format('.02f'));

    d3.select('#chart_div svg')
      .datum(chartData)
      .call(chart);

    nv.utils.windowResize(chart.update);

    return chart;
  });

  // Dragable Panel
  // --------------
  // Use JQuery UI to drag panel
  $('#controls').draggable({
    handle: '.panel-heading'
  });

  // MQTT Client
  // -----------

  // Generate a five character client ID suffix.
  var clientID = (function() {
    var id = 'viewer-';
    for(var i = 0; i < 10; i++) {
      id += Math.floor(Math.random() * 16).toString(16);
    }
    return id;
  })();

  // Dictionary for message handlers.
  var handlerDictionary = {};

  // Initialise a conneciton to the MessageSight broker.
  var mqttClient = new Messaging.Client('5.153.17.246', 13531, clientID);
  console.log('Connecting with client ID: ' + clientID);
  mqttClient.connect({
    onSuccess: function() {
      console.log('MQTT Client Connected');
      mqttClient.subscribe('/test/debug');
    }
  });

  // On message arrival, lookup trace in dictionary for handler.
  mqttClient.onMessageArrived = function(msg) {
    var dest = msg.destinationName;
    if (dest.indexOf('/trace/') === 0) {
      var traceID = dest.substr('/trace/'.length);
      if (traceID in handlerDictionary) {
        handlerDictionary[traceID](JSON.parse(msg.payloadString));
      } else {
        console.log('Unknown traceID: ' + traceID);
      }
    } else {
      console.log('Unknown topic:');
      console.log(msg);
    }
  };

  // Plotting traces
  // ===============
  // Bounds for map data.
  var mapBounds = new google.maps.LatLngBounds();

  // Trace Class
  // -----------
  var Trace = function() {
    var self = this;

    // Color representing this trace.
    var color = nextColor();

    // Table row for this trace.
    var symbolCell = $('').html('●')
                   .css('color', color);
    var nameCell = $('Loading...	');
    var startCell = $('');
    var totalCell = $('Live	');
    $('#traceListing').append(
      $('
').append(symbolCell, nameCell, startCell, totalCell)
    );

    // Polyline for Google Maps.
    self.polyLine = new google.maps.Polyline({
      strokeColor: color,
      strokeOpacity: 0.7,
      strokeWeight: 4,
      map: map
    });

    // Marker for Google Maps.
    var iconUrl = 'http://chart.apis.google.com/chart?chst=d_map_pin_letter&chld=%E2%80%A2|' + color.substr(1);
    self.marker = new google.maps.Marker({
      title: 'Loading',
      icon: iconUrl,
      map: map
    });

    // Chart data for nvd3.
    self.seriesData = {
      values: [],
      key: 'Loading...',
      color: color
    };
    chartData.push(self.seriesData);

    // Change title of the trace.
    self.setTitle = function(title) {
      nameCell.text(title);
      self.seriesData.key = title;
      self.marker.setTitle(title);
    };

    // Add datapoint.
    var lastMinute = null;
    self.addDataPoint = function(time, point) {
      // Add to polyline
      var path = self.polyLine.getPath();
      path.push(point);
      self.polyLine.setPath(path);

      // Move marker
      self.marker.setPosition(point);

      // If initial datapoint, set start-time
      if (path.length === 1) {
        self.startTime = moment(time);
        startCell.text(self.startTime.calendar());
      }

      // Extend map bounds
      mapBounds.extend(point);
      map.fitBounds(mapBounds);

      // Add to Speed vs. Time graph.
      if (lastMinute) {
        if (1 <= moment(time).diff(lastMinute.time, 'minutes', true)) {
          var timeDelta = moment(time).diff(lastMinute.time, 'hours', true);
          var distDelta = google.maps.geometry.spherical
                        .computeDistanceBetween(lastMinute.point, point) / 1000;
          var speed = distDelta / timeDelta;

          self.seriesData.values.push({
            x: moment(lastMinute.time).diff(self.startTime, 'minutes', true),
            y: speed
          });
          lastMinute = {time: time, point: point};
        }
      } else {
        lastMinute = {time: time, point: point};
      }
    };

    // Subscribe to live feed
    self.subscribeFeed = function(traceID) {
      handlerDictionary[traceID] = function(obj) {
        self.addDataPoint(
          moment.unix(obj.tst),
          new google.maps.LatLng(obj.lat, obj.lon)
        );
        chart.update();
      };
      mqttClient.subscribe('/trace/' + traceID);
    };

    // Finish a live feed
    self.finishFeed = function() {
      var delta = moment(lastMinute.time).diff(self.startTime, 'hours', true).toFixed(2);
      nameCell.removeClass('text-success');
      totalCell.text(delta + ' hours').removeClass('text-success');
    };
  };

  // Parse a GPX file
  var parseGPX = function(txt) {
    var $xml = $(txt);
    var $trkpts = $xml.children('trk')
                .children('trkseg').children('trkpt');

    var startDate, endDate;
    var points = [];

    $trkpts.each(function() {
        var lat = $(this).attr('lat');
        var lon = $(this).attr('lon');
        var tst = moment($(this).find('time').text()).format('X');

        if (!startDate) startDate = tst;
        endDate = tst;
        points.push({tst: tst, lat: lat, lon: lon});
    });

    return {
      title: $xml.children('trk').children('name').text(),
      startDate: startDate,
      endDate: endDate,
      points: points
    };
  };

  // Function for plotting new traces from archive telemetry data
  var plotArchive = function(data) {
    var trace = new Trace();

    data.points.forEach(function(item) {
      var point = new google.maps.LatLng(item.lat, item.lon);
      trace.addDataPoint(moment.unix(item.tst), point);
    });

    trace.setTitle(data.title);
    chart.update();
    console.log('Done.');

    return trace;
  };
});<commit_msg>Many updates to add trace<commit_after>/* global $, google, nv, d3, moment, console, Messaging */
'use strict';

// Wait until document load.
$(function() {
  // Initialise Globals
  // ==================

  // Google Maps
  // -----------
  // Google Maps object, centred on York.
  var map = new google.maps.Map($('#mapCanvas').get(0), {
    center: new google.maps.LatLng(53.958333, -1.080278),
    zoom: 11
  });

  // Trace Colors
  // ------------
  // Function that returns next color from cyclical queue.
  var nextColor = (function() {
    var colorQueue = ['#E84A21', '#5BFF75', '#FFF84E', '#E80C7A', '#1146FF'];
    return function() {
      var temp = colorQueue.shift();
      colorQueue.push(temp);
      return temp;
    };
  })();

  // MQTT Client
  // -----------

  // Generate a five character client ID suffix.
  var clientID = (function() {
    var id = 'viewer-';
    for(var i = 0; i < 10; i++) {
      id += Math.floor(Math.random() * 16).toString(16);
    }
    return id;
  })();

  // Dictionary for message handlers.
  var handlerDictionary = {};

  // Initialise a conneciton to the MessageSight broker.
  var mqttClient = new Messaging.Client('5.153.17.246', 13531, clientID);
  console.log('Connecting with client ID: ' + clientID);
  mqttClient.connect({
    onSuccess: function() {
      console.log('MQTT Client Connected');
      mqttClient.subscribe('/test/debug');
    }
  });

  // On message arrival, lookup trace in dictionary for handler.
  mqttClient.onMessageArrived = function(msg) {
    var dest = msg.destinationName;
    if (dest.indexOf('/trace/') === 0) {
      var traceID = dest.substr('/trace/'.length);
      if (traceID in handlerDictionary) {
        handlerDictionary[traceID](JSON.parse(msg.payloadString));
      } else {
        console.log('Unknown traceID: ' + traceID);
      }
    } else {
      console.log('Unknown topic:');
      console.log(msg);
    }
  };

  // Send command (requires connection)
  var sendCommand = function (obj) {
    var message = new Messaging.Message(JSON.stringify(obj));
    message.destinationName = "/command/";
    message.qos = 1;
    mqttClient.send(message);
  };

  // Add trace
  // ---------

  // Add Trace form validity
  var isAddTraceFormValid = function() {
    // Page 2: Can only view in full if loading from disk.
    if ($('#telemetryViewRadioPartial').prop('checked')) {
      if ($('#telemetrySourceRadioLocal').prop('checked')) {
        return 'Cannot replay trace without first uploading to server.';
      }
    }

    // Page 1: Are boxes filled?
    if ($('#telemetrySourceRadioLocal').prop('checked')) {
      if ($('#uploadFile').get(0).files.length !== 1) {
        return 'No source file selected.';
      }
    } else if ($('#telemetrySourceRadioRemote').prop('checked')) {
      if (! $('#remoteID').val()) {
        return 'No CouchDB source ID entered.';
      }
    } else if ($('#telemetrySourceRadioMQTT').prop('checked')) {
      if (! $('#mqttSource').val()) {
        return 'No MQTT source topic entered.';
      }
    }

    // Page 2: Are boxes filled?
    if ($('#telemetryViewRadioPartial').prop('checked')) {
      if (! $('#startTimePartial').val()) {
        return 'No start time entered.';
      }
    }

    // Page 3: Are boxes filled?
    if ($('#telemetryTargetCouchDB').prop('checked')) {
      if (! $('#storeID').val()) {
        return 'No couch DB storage ID entered.';
      }
    } else if ($('#telemetryTargetMQTT').prop('checked')) {
      if (! $('#mqttTarget').val()) {
        return 'No MQTT topic topic entered.';
      }
    }
  };

  $('#addAccordian').change(function() {
    var isValid = isAddTraceFormValid();
    if (isValid) {
      $('#validationError').text(isValid);
    } else {
      $('#validationError').text("");
    }
  });

  var doSource = function() {
    if ($('#telemetrySourceRadioLocal').prop('checked')) {
      // Load the file from disk
      var reader = new FileReader();
      reader.onload = function(event) {
        doTarget(parseGPX(event.target.result), null);
      }
      reader.readAsText($('#uploadFile').get(0).files[0]);
    } else if ($('#telemetrySourceRadioRemote').prop('checked')) {
      var topicSegment = $('#remoteID').val();
      if ($('#telemetryViewRadioFull').prop('checked')) {
        $.ajax({
          url: '/db/traces/_design/TraceDesign/_view/getTraceByTopic?key='
               + encodeURIComponent('"' + topicSegment + '"'),
          type: 'GET',
          contentType: 'application/json',
          dataType: 'json',
          success: function(traceData) {
            if (traceData.total_rows == 1) {
              $.ajax({
                url: '/db/points/_design/PointsDesign/_view/getPointsByTopic?key='
                     + encodeURIComponent('"' + topicSegment + '"'),
                type: 'GET',
                contentType: 'application/json',
                dataType: 'json',
                success: function(pointsData) {
                  var history = {
                    title: traceData.rows[0].value.title,
                    startTst: traceData.rows[0].value.startTst,
                    endTst: traceData.rows[0].value.endTst,
                    points: []
                  };

                  pointsData.rows.forEach( function(item) {
                    history.points.push(item.value);
                  });

                  doTarget(history, null);
                }
              });
            };
          }
        });
      } else if ($('#telemetryViewRadioPartial').prop('checked')) {
        var history = {
          title: '/replay/' + topicSegment,
          points: []
        };
        sendCommand({command: 'load', topic: topicSegment});
        sendCommand({command: 'start', topic: topicSegment});
        doTarget(history, topicSegment);
      }
    } else if ($('#telemetrySourceRadioMQTT').prop('checked')) {
      var mqttTopic = $('#mqttSource').val();
      var history = {
        title: mqttTopic,
        startDate: moment().format('X'),
        points: []
      };
      doTarget(history, mqttTopic);
    }
  };

  var doTarget = function(history, mqttTopic) {
    if ($('#telemetryTargetCouchDB').prop('checked')) {
      var traceID = $('#storeID').val();
      var points = [];
      history.points.forEach(function(item) {
        item._id = traceID + item.tst;
        item.topic = traceID;
        points.push(item);
      });

      console.log('POSTing bulk points data.');
      $.ajax({
        url: '/db/points/_bulk_docs',
        type: 'POST',
        data: JSON.stringify({docs: points}),
        contentType: 'application/json',
        dataType: 'json',
        complete: function(jqXHR, textStatus) {
          console.log('Points: ');
          console.log(textStatus);
        }
      });

      $.ajax({
        url: '/db/traces',
        type: 'POST',
        data: JSON.stringify({
          startTst:  history.startDate,
          endTst:  history.endDate,
          title:  history.title,
          topic: traceID
        }),
        contentType: 'application/json',
        dataType: 'json',
        complete: function(jqXHR, textStatus) {
          console.log('Trace: ');
          console.log(textStatus);
        }
      });
    }

    if ($('#telemetryTargetMQTT').prop('checked')) {
      mqttTopic = '/trace/' + $('#mqttTarget').val();
    }

    if ($('#telemetryTargetMap').prop('checked')) {
      var trace = plotArchive(history);
      if (mqttTopic) {
        trace.subscribeFeed(mqttTopic);
      } else {
        trace.finishFeed();
      }
    }

    // Finally, hide dialog.
    $('#addTrace').modal('hide');
  };

  // Save button on add trace.
  $('#addTraceSave').click(function() {
    if (!isAddTraceFormValid()) {
      doSource();
    }
  });

  // Speed vs. Time Chart
  // --------------------

  // Chart object.
  var chart;

  // Chart data object.
  var chartData = [];

  // Create chart.
  nv.addGraph(function() {
    chart = nv.models.lineChart()
      .options({
        x: function(d,i) { return i; },
        showXAxis: true,
        showYAxis: true,
        showLegend: false,
        transitionDuration: 250,
        useInteractiveGuideline: true
      });

    chart.xAxis
      .axisLabel('Minutes')
      .tickFormat(d3.format('d'));

    chart.yAxis
      .axisLabel('Speed (km/h)')
      .axisLabelDistance(30)
      .tickFormat(d3.format('.02f'));

    d3.select('#chart_div svg')
      .datum(chartData)
      .call(chart);

    nv.utils.windowResize(chart.update);

    return chart;
  });

  // Dragable Panel
  // --------------
  // Use JQuery UI to drag panel
  $('#controls').draggable({
    handle: '.panel-heading'
  });

  // Plotting traces
  // ===============
  // Bounds for map data.
  var mapBounds = new google.maps.LatLngBounds();

  // Trace Class
  // -----------
  var Trace = function() {
    var self = this;

    // Color representing this trace.
    var color = nextColor();

    // Table row for this trace.
    var symbolCell = $('').html('●')
                   .css('color', color);
    var nameCell = $('Loading...	');
    var startCell = $('');
    var totalCell = $('Live	');
    $('#traceListing').append(
      $('
').append(symbolCell, nameCell, startCell, totalCell)
    );

    // Polyline for Google Maps.
    self.polyLine = new google.maps.Polyline({
      strokeColor: color,
      strokeOpacity: 0.7,
      strokeWeight: 4,
      map: map
    });

    // Marker for Google Maps.
    var iconUrl = 'http://chart.apis.google.com/chart?chst=d_map_pin_letter&chld=%E2%80%A2|' + color.substr(1);
    self.marker = new google.maps.Marker({
      title: 'Loading',
      icon: iconUrl,
      map: map
    });

    // Chart data for nvd3.
    self.seriesData = {
      values: [],
      key: 'Loading...',
      color: color
    };
    chartData.push(self.seriesData);

    // Change title of the trace.
    self.setTitle = function(title) {
      nameCell.text(title);
      self.seriesData.key = title;
      self.marker.setTitle(title);
    };

    // Add datapoint.
    var lastMinute = null;
    self.addDataPoint = function(time, point) {
      // Add to polyline
      var path = self.polyLine.getPath();
      path.push(point);
      self.polyLine.setPath(path);

      // Move marker
      self.marker.setPosition(point);

      // If initial datapoint, set start-time
      if (path.length === 1) {
        self.startTime = moment(time);
        startCell.text(self.startTime.calendar());
      }

      // Extend map bounds
      mapBounds.extend(point);
      map.fitBounds(mapBounds);

      // Add to Speed vs. Time graph.
      if (lastMinute) {
        if (1 <= moment(time).diff(lastMinute.time, 'minutes', true)) {
          var timeDelta = moment(time).diff(lastMinute.time, 'hours', true);
          var distDelta = google.maps.geometry.spherical
                        .computeDistanceBetween(lastMinute.point, point) / 1000;
          var speed = distDelta / timeDelta;

          self.seriesData.values.push({
            x: moment(lastMinute.time).diff(self.startTime, 'minutes', true),
            y: speed
          });
          lastMinute = {time: time, point: point};
        }
      } else {
        lastMinute = {time: time, point: point};
      }
    };

    // Subscribe to live feed
    self.subscribeFeed = function(traceID) {
      handlerDictionary[traceID] = function(obj) {
        self.addDataPoint(
          moment.unix(obj.tst),
          new google.maps.LatLng(obj.lat, obj.lon)
        );
        chart.update();
      };
      mqttClient.subscribe('/trace/' + traceID);
    };

    // Finish a live feed
    self.finishFeed = function() {
      var delta = moment(lastMinute.time).diff(self.startTime, 'hours', true).toFixed(2);
      nameCell.removeClass('text-success');
      totalCell.text(delta + ' hours').removeClass('text-success');
    };
  };

  // Parse a GPX file
  var parseGPX = function(txt) {
    var $xml = $(txt);
    var $trkpts = $xml.children('trk')
                .children('trkseg').children('trkpt');

    var startDate, endDate;
    var points = [];

    $trkpts.each(function() {
        var lat = $(this).attr('lat');
        var lon = $(this).attr('lon');
        var tst = moment($(this).find('time').text()).format('X');

        if (!startDate) startDate = tst;
        endDate = tst;
        points.push({tst: tst, lat: lat, lon: lon});
    });

    return {
      title: $xml.children('trk').children('name').text(),
      startDate: startDate,
      endDate: endDate,
      points: points
    };
  };

  // Function for plotting new traces from archive telemetry data
  var plotArchive = function(data) {
    var trace = new Trace();

    data.points.forEach(function(item) {
      var point = new google.maps.LatLng(item.lat, item.lon);
      trace.addDataPoint(moment.unix(item.tst), point);
    });

    trace.setTitle(data.title);
    chart.update();
    console.log('Done.');

    return trace;
  };
});<|endoftext|>