(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.jiboTunable = f()}})(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o */ "use strict"; exports.ReqType = { INIT: 'INIT', NEW_FIELD: 'NEW_FIELD', PLAY: 'PLAY', PLAYING: 'PLAYING', DONE_PLAYING: 'DONE_PLAYING', VALUE_UPDATE: 'VALUE_UPDATE', }; var gui; var Client = (function () { function Client(port) { var _this = this; this.wsHandlers = new Map(); this.folders = new Map(); // Setup gui for first time if (!gui) { gui = new dat.GUI({ autoPlace: false }); var container = document.getElementById('gui-container'); container.appendChild(gui.domElement); gui.width = 400; } this.ws = new WebSocket('ws://' + window.location.hostname + ':' + port); // When the connection is open, send some data to the server this.ws.onopen = function () { var msg = { type: exports.ReqType.INIT, data: '' }; _this.sendWS(msg); }; this.ws.onclose = function (ev) { console.log('WebSocket closed: ', ev); _this.cleanup(); }; // Log errors this.ws.onerror = function (error) { console.error('WebSocket Error: ', error); _this.cleanup(); }; // Log messages from the server this.ws.onmessage = function (incoming) { console.log('Got WS message: ' + incoming.data); var msg = JSON.parse(incoming.data); var handler = _this.wsHandlers.get(msg.type); if (handler) { var resp = handler(msg.data); if (resp) { _this.sendWS(resp); } } }; this.addWsHandler(exports.ReqType.VALUE_UPDATE, function (data) { var folder = _this.folders.get(data.folderName); if (!folder) { throw new Error("No folder of name '" + data.folderName + "'"); } folder.obj[data.fieldName] = data.value; // Update display value of all children of folder folder.gui.__controllers.forEach(function (c) { c.updateDisplay(); }); }); /** * Install handler for new fields */ this.addWsHandler(exports.ReqType.NEW_FIELD, function (data) { var folderName = data.folderName; var fieldName = data.fieldName; var folder = _this.folders.get(folderName); if (!folder) { folder = { obj: {}, gui: gui.addFolder(folderName) }; _this.folders.set(folderName, folder); } folder.gui.open(); folder.obj[fieldName] = data.field._current; var el; var type = data.field.type; if (type === 'string') { el = folder.gui.add(folder.obj, fieldName, data.field.options); } else if (type === 'boolean') { el = folder.gui.add(folder.obj, fieldName); } else if (type === 'number') { el = folder.gui.add(folder.obj, fieldName, data.field.min, data.field.max); el.step(data.field.step); } else if (type === 'button') { folder.obj[fieldName] = function () { //let a = arguments; //debugger; //this.sendWS({ // type: ReqType.VALUE_UPDATE, // data: { // folderName, // fieldName, // value: true // } //}); }; el = folder.gui.add(folder.obj, fieldName); } else { throw new Error("Can't handle field of type '" + data.field.type + "'"); } _this.addChangeListener(el, folderName, fieldName); }); } Client.prototype.sendWS = function (msg) { this.ws.send(JSON.stringify(msg)); }; Client.prototype.addWsHandler = function (type, handler) { this.wsHandlers.set(type, handler); }; Client.prototype.addChangeListener = function (el, folderName, fieldName) { var _this = this; el.onChange(function (value) { _this.sendWS({ type: exports.ReqType.VALUE_UPDATE, data: { folderName: folderName, fieldName: fieldName, value: value, } }); }); }; Client.prototype.cleanup = function () { if (gui) { this.folders.forEach(function (folder, key) { var clone = folder.gui.__controllers.slice(0); clone.forEach(function (fCon) { return folder.gui.remove(fCon); }); }); } this.folders.clear(); gui.destroy(); gui = undefined; }; return Client; }()); exports.Client = Client; var root = window || global; root.Client = Client; },{}],2:[function(require,module,exports){ /** * @fileOverview * * Created on 5/21/16. * @author Siggi Orn */ "use strict"; var __extends = (this && this.__extends) || function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; function __() { this.constructor = d; } d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); }; var jibo_typed_events_1 = require("jibo-typed-events"); var Client_1 = require("./Client"); var FieldEvents = (function (_super) { __extends(FieldEvents, _super); function FieldEvents() { var _this = _super.call(this) || this; _this.change = new jibo_typed_events_1.Event('Value change'); return _this; } return FieldEvents; }(jibo_typed_events_1.EventContainer)); exports.FieldEvents = FieldEvents; var Field = (function () { function Field(folder, name, initial, type) { this.folder = folder; this.name = name; this.initial = initial; this.type = type; this.events = new FieldEvents(); this._setCurrent(initial, false, true); } Object.defineProperty(Field.prototype, "current", { get: function () { return this._current; }, set: function (value) { this._setCurrent(value, false); }, enumerable: true, configurable: true }); Field.prototype._setCurrent = function (value, fromRemote, noChange) { if (noChange === void 0) { noChange = false; } var forceChangeEvent = false; if (this._limitInput) { value = this._limitInput(value); if (value === null) { forceChangeEvent = true; } } var change = this._current !== value; this._current = value; if (!fromRemote) { this._updateRemote(); } if ((forceChangeEvent || change) && !noChange) { this.events.change.emit(this._current); } }; Field.prototype._updateRemote = function () { // Update remote this.folder.server.sendWS(null, { type: Client_1.ReqType.VALUE_UPDATE, data: { fieldName: this.name, folderName: this.folder.name, value: this._current } }); }; /** * Returns a basic representation of this element for serialization * @returns {any} */ Field.prototype.toSimpleObject = function () { var out = {}; for (var _i = 0, _a = Object.keys(this); _i < _a.length; _i++) { var attr = _a[_i]; if (attr !== 'events' && attr !== 'folder') { out[attr] = this[attr]; } } return out; }; return Field; }()); exports.Field = Field; var StringField = (function (_super) { __extends(StringField, _super); function StringField(folder, name, initial) { if (initial === void 0) { initial = ''; } var _this = _super.call(this, folder, name, initial, 'string') || this; _this.initial = initial; return _this; } return StringField; }(Field)); exports.StringField = StringField; var DropdownField = (function (_super) { __extends(DropdownField, _super); function DropdownField(folder, name, options, initialIndex) { if (initialIndex === void 0) { initialIndex = 0; } var _this = _super.call(this, folder, name, (options[initialIndex] || ''), 'string') || this; _this.options = options; if (initialIndex < 0 || initialIndex >= options.length) { throw Error("Tunable: '" + _this.folder.name + "/" + _this.name + "' initial index " + initialIndex + " out of bounds"); } _this._limitInput = function (input) { var index = options.indexOf(input); if (index === -1) { console.warn("Tunable: '" + _this.folder.name + "/" + _this.name + "' input " + input + " not a valid option, skipping"); input = _this.current; } return input; }; return _this; } return DropdownField; }(Field)); exports.DropdownField = DropdownField; var CheckboxField = (function (_super) { __extends(CheckboxField, _super); function CheckboxField(folder, name, initial) { if (initial === void 0) { initial = false; } var _this = _super.call(this, folder, name, initial, 'boolean') || this; _this.initial = initial; return _this; } return CheckboxField; }(Field)); exports.CheckboxField = CheckboxField; var NumberField = (function (_super) { __extends(NumberField, _super); function NumberField(folder, name, initial, min, max, step) { if (initial === void 0) { initial = 0; } if (step === void 0) { step = -1; } var _this = _super.call(this, folder, name, initial, 'number') || this; _this.initial = initial; _this.min = min; _this.max = max; _this.step = step; if (min > initial || max < initial) { throw new Error("Invalid bounds, min: " + min + ", max: " + max + ", initial: " + initial); } if (step === -1) { _this.step = (max - min) / 100; } _this._limitInput = function (input) { if (input < _this.min) { console.warn("Tunable: '" + _this.folder.name + "/" + _this.name + "' input " + input + " lower than min " + _this.min + ", capping"); input = _this.min; } else if (input > _this.max) { console.warn("Tunable: '" + _this.folder.name + "/" + _this.name + "' input " + input + " higher than max " + _this.max + ", capping"); input = _this.max; } return input; }; return _this; } return NumberField; }(Field)); exports.NumberField = NumberField; var ButtonField = (function (_super) { __extends(ButtonField, _super); function ButtonField(folder, name) { var _this = _super.call(this, folder, name, false, 'button') || this; // Sending null to enforce a change event _this._limitInput = function () { return null; }; return _this; } ButtonField.prototype.press = function () { this.current = true; }; return ButtonField; }(Field)); exports.ButtonField = ButtonField; },{"./Client":1,"jibo-typed-events":undefined}],3:[function(require,module,exports){ /** * @fileOverview * * Created on 5/21/16. * @author Siggi Orn */ "use strict"; var Folder = (function () { function Folder(name, server) { this.name = name; this.server = server; this.fields = new Map(); } Folder.prototype._installField = function (name, creator) { var el = this.fields.get(name); if (el) { return el; } el = creator(); this.server.notifyClientOfNewField(null, this.name, name, el); this.fields.set(name, el); return el; }; Folder.prototype.getFieldOrThrow = function (name) { var el = this.fields.get(name); if (!el) { throw new Error("No string field named '" + name + "'"); } return el; }; Folder.prototype.getValue = function (name) { return this.getFieldOrThrow(name).current; }; return Folder; }()); exports.Folder = Folder; },{}],4:[function(require,module,exports){ "use strict"; var path = require("path"); var jibo_cai_utils_1 = require("jibo-cai-utils"); var express = require('express'); var ws = require('nodejs-websocket'); var Folder_1 = require("./Folder"); var Client_1 = require("./Client"); jibo_cai_utils_1.PromiseUtils.catchUnhandledRejection(); var root = jibo_cai_utils_1.FileUtils.getProjectRoot(__dirname); var shouldLog = false; function log() { var args = []; for (var _i = 0; _i < arguments.length; _i++) { args[_i] = arguments[_i]; } if (shouldLog) { var func = console.log; func.apply(void 0, args); } } var Server = (function () { function Server(port) { var _this = this; this.port = port; this.expressApp = express(); this.wsConnections = new Set(); this.wsHandlers = new Map(); this.folders = new Map(); /** * Set up Webserver */ // Handle root this.expressApp.get('/', function (req, res) { jibo_cai_utils_1.FileUtils.readFile(root + '/public/index.html') .then(function (data) { data = data.replace(RegExp('PORT_TO_USE'), '' + (_this.port + 1)); res.send(data); }) .catch(function (e) { return res.sendStatus(404); }); }); // Get certain files from transpiled directory this.expressApp.get('/jibo-tunable-client.js', function (req, res) { var filename = path.basename(req.path); log('Request for transpiled file: ' + filename); jibo_cai_utils_1.FileUtils.readFile(root + '/lib/' + filename) .then(function (data) { return res.send(data); }) .catch(function (e) { return res.sendStatus(404); }); }); // Host files from public directory this.expressApp.get('/**.*', function (req, res) { var filename = path.basename(req.path); log('Request for regular file: ' + filename); jibo_cai_utils_1.FileUtils.readFile(root + '/public/' + filename) .then(function (data) { res.send(data); }) .catch(function (e) { return res.sendStatus(404); }); }); /** * Set up websockets */ this.webSocketServer = ws.createServer(function (wsConn) { _this.wsConnections.add(wsConn); log('WS Connection opened'); wsConn.on('text', function (str) { log('Got WS message: ' + str); var req = JSON.parse(str); var handler = _this.wsHandlers.get(req.type); if (handler) { var resp = handler(wsConn, req.data); if (resp) { _this.sendWS(wsConn, resp); } } }); wsConn.on("close", function (code, reason) { _this.wsConnections.delete(wsConn); log("WS Connection closed"); }); }); /** * Here we add some handlers */ // On INIT we send all fields to client this.addWsMessageHandler(Client_1.ReqType.INIT, function (wsConn, data) { _this.folders.forEach(function (folder, folderName) { folder.fields.forEach(function (field, fieldName) { _this.notifyClientOfNewField(wsConn, folderName, fieldName, field); }); }); }); // On value update we update values this.addWsMessageHandler(Client_1.ReqType.VALUE_UPDATE, function (wsConn, data) { var folder = _this.folders.get(data.folderName); if (!folder) { throw new Error("No folder of name '" + data.folderName + "'"); } var field = folder.fields.get(data.fieldName); if (!field) { throw new Error("No field of name '" + data.fieldName + "'"); } field._setCurrent(data.value, true); }); } Server.prototype.notifyClientOfNewField = function (wsConn, folderName, fieldName, field) { var _this = this; var msg = { type: Client_1.ReqType.NEW_FIELD, data: { folderName: folderName, fieldName: fieldName, field: field.toSimpleObject(), } }; if (wsConn) { this.sendWS(wsConn, msg); } else { this.wsConnections.forEach(function (_wsConn) { _this.sendWS(_wsConn, msg); }); } }; Server.prototype.getFolder = function (name) { var nameToUse = (name === '') ? 'Tunable Parameters' : name; var folder = this.folders.get(nameToUse); if (!folder) { folder = new Folder_1.Folder(nameToUse, this); this.folders.set(nameToUse, folder); } return folder; }; Server.prototype.start = function () { var _this = this; var errHandler = function (e) { console.error("jibo-tunable error: ", e); _this.close(); }; // Start servers this.httpServer = this.expressApp.listen(this.port); this.httpServer.on('error', errHandler); this.webSocketServer.listen(this.port + 1); this.webSocketServer.on('error', errHandler); log('Tunable server started on port ' + this.port); }; Server.prototype.close = function () { if (this.httpServer) { this.httpServer.close(); this.httpServer = null; } if (this.webSocketServer) { this.webSocketServer.close(); this.webSocketServer = null; } }; Server.prototype.broadcastWS = function (msg) { var _this = this; this.wsConnections.forEach(function (ws) { _this.sendWS(ws, msg); }); }; Server.prototype.sendWS = function (wsConn, msg) { var json = JSON.stringify(msg); if (!wsConn) { this.wsConnections.forEach(function (_wsConn) { _wsConn.sendText(json); }); } else { wsConn.sendText(json); } }; Server.prototype.addWsMessageHandler = function (type, handler) { this.wsHandlers.set(type, handler); }; return Server; }()); exports.Server = Server; },{"./Client":1,"./Folder":3,"express":undefined,"jibo-cai-utils":undefined,"nodejs-websocket":undefined,"path":undefined}],5:[function(require,module,exports){ /** * @fileOverview * * Created on 5/21/16. * @author Siggi Orn */ "use strict"; var Server_1 = require("./Server"); var Field_1 = require("./Field"); function shutDownHandler(message) { Tunable.close(); } process.on('exit', function () { return shutDownHandler('process exit'); }); process.on('SIGINT', function () { return shutDownHandler('SIGINT'); }); process.on('SIGTERM', function () { return shutDownHandler('SIGTERM'); }); /** * Default server port * @type {number} */ var DEFAULT_PORT = 3333; var server; var Tunable = (function () { function Tunable() { } /** * Initialize server at port number * This method is optional but can be used to start a server * at a non-default port, it has to be called before any other * method here. * @param {number} port Server port */ Tunable.initializeServer = function (port) { if (server) { server.close(); } server = new Server_1.Server(port); server.start(); }; /** * Stops both the web and websocket servers */ Tunable.close = function () { if (server) { server.close(); } }; /** * Gets or starts the server, and then gets or creates a folder * with the requested name * @param name * @returns {Folder} */ Tunable.getFolder = function (name) { if (name === void 0) { name = ''; } if (!server) { Tunable.initializeServer(DEFAULT_PORT); } return server.getFolder(name); }; /** * Creates or gets the already created string field for name * @param {string} name * @param {string} [initial=''] * @param {string} [folderName=''] Name of GUI folder to use (empty by default) * @returns {StringField} */ Tunable.getStringField = function (name, initial, folderName) { if (initial === void 0) { initial = ''; } if (folderName === void 0) { folderName = ''; } var folder = Tunable.getFolder(folderName); return folder._installField(name, function () { return new Field_1.StringField(folder, name, initial); }); }; /** * Creates or gets the already created dropdown field for name * @param {string} name * @param {string[]} options * @param {number} [initialIndex=0] * @param {string} [folderName=''] Name of GUI folder to use (empty by default) * @returns {DropdownField} */ Tunable.getDropdownField = function (name, options, initialIndex, folderName) { if (initialIndex === void 0) { initialIndex = 0; } if (folderName === void 0) { folderName = ''; } var folder = Tunable.getFolder(folderName); return folder._installField(name, function () { return new Field_1.DropdownField(folder, name, options, initialIndex); }); }; /** * Creates (or gets the already created) checkbox field for name * @param {string} name * @param {boolean} [initial=false] * @param {string} [folderName=''] Name of GUI folder to use (empty by default) * @returns {CheckboxField} */ Tunable.getCheckboxField = function (name, initial, folderName) { if (initial === void 0) { initial = false; } if (folderName === void 0) { folderName = ''; } var folder = Tunable.getFolder(folderName); return folder._installField(name, function () { return new Field_1.CheckboxField(folder, name, initial); }); }; /** * Creates (or gets the already created) number field for name * @param {string} name * @param {number} initial Initial value for number * @param {number} min Minimum value for number * @param {number} max Maximum value for number * @param {number} [step=-1] Increments * @param {string} [folderName=''] Name of GUI folder to use (empty by default) * @returns {NumberField} */ Tunable.getNumberField = function (name, initial, min, max, step, folderName) { if (step === void 0) { step = -1; } if (folderName === void 0) { folderName = ''; } var folder = Tunable.getFolder(folderName); return folder._installField(name, function () { return new Field_1.NumberField(folder, name, initial, min, max, step); }); }; /** * Creates (or gets the already created) trigger button field for name * @param {string} name * @param {string} [folderName=''] Name of GUI folder to use (empty by default) * @returns {ButtonField} */ Tunable.getButtonField = function (name, folderName) { if (folderName === void 0) { folderName = ''; } var folder = Tunable.getFolder(folderName); return folder._installField(name, function () { return new Field_1.ButtonField(folder, name); }); }; /** * Gets the current value for a field of 'name' * @param {string} name * @param {string} [folderName=''] Name of GUI folder to use (empty by default) * @returns {FieldType} * @throws Error if no such field exists */ Tunable.getValue = function (name, folderName) { if (folderName === void 0) { folderName = ''; } var folder = Tunable.getFolder(folderName); return folder.getValue(name); }; return Tunable; }()); exports.Tunable = Tunable; },{"./Field":2,"./Server":4}],6:[function(require,module,exports){ /** * @fileOverview * * Created on 4/11/16. * @author Siggi Orn */ "use strict"; var Tunable_1 = require("./Tunable"); exports.Tunable = Tunable_1.Tunable; },{"./Tunable":5}],7:[function(require,module,exports){ /** * @fileOverview * * Created on 4/11/16. * @author Siggi Orn */ var root = global; if (!root.__tunableSingleton) { root.__tunableSingleton = require('./index-internal'); } module.exports = root.__tunableSingleton; },{"./index-internal":6}]},{},[7])(7) }); //# sourceMappingURL=jibo-tunable.js.map