public/src/SAGE2_WidgetControl.js
// SAGE2 is available for use under the SAGE2 Software License
//
// University of Illinois at Chicago's Electronic Visualization Laboratory (EVL)
// and University of Hawai'i at Manoa's Laboratory for Advanced Visualization and
// Applications (LAVA)
//
// See full text, terms and conditions in the LICENSE.txt included file
//
// Copyright (c) 2014
/* global SAGE2WidgetButtonTypes */
/* global polarToCartesian */
"use strict";
/**
* Provides widget controls and helper functionality for custom application user interface
*
* @module client
* @submodule widgets
*/
/**
* Widget Control: has functions exposed to SAGE2 apps for adding elements onto the widget bar
*
* @class SAGE2WidgetControl
* @constructor
* @param id {String} identifier for the object
* @return {Object} an object representing a widget control bar
*/
function SAGE2WidgetControl(id) {
this.ButtonClass = function() {
this.appId = null;
this.id = null;
this.type = null;
this.call = null;
};
this.SliderClass = function() {
this.id = null;
this.appId = null;
this.begin = null;
this.end = null;
this.increments = null;
this.parts = null;
this.call = null;
this.appHandle = null;
this.appProperty = null;
this.sliderVal = null;
};
this.TextInputClass = function() {
this.id = null;
this.appId = null;
this.width = null;
};
this.LabelClass = function() {
this.id = null;
this.appHandle = null;
this.appId = null;
this.width = null;
this.appProperty = null;
};
this.RadioButton = function() {
this.id = null;
this.appId = null;
this.data = {
options: [],
value: null
};
};
this.ColorPaletteClass = function() {
this.id = null;
this.appId = null;
this.colorList = [];
};
this.id = id;
this.instanceID = "";
this.specReady = false;
this.itemCount = 0;
this.sideBarElements = [];
this.buttonSequence = {};
this.separatorList = [];
this.buttonType = SAGE2WidgetButtonTypes;
this.layoutOptions = {
drawGroupBoundaries: false,
drawBackground: true,
shape: "radial",
drawSpokes: true,
drawSquareButtons: false
};
}
/*
* Ensures everything got added to the controls specification properly
*/
SAGE2WidgetControl.prototype.finishedAddingControls = function() {
this.specReady = true;
};
/*
* Check whether control specification is ready (used before creating widget elements from specification)
*/
SAGE2WidgetControl.prototype.controlsReady = function() {
return this.specReady;
};
/*
* Lets the user add a custom cover for buttons
* Added cover is available only to that instance of that app.
*/
SAGE2WidgetControl.prototype.addButtonType = function(type, buttonData) {
if (this.buttonType[type] === undefined || this.buttonType[type] === null) {
this.buttonType[type] = function() {
this.state = buttonData.state;
this.from = buttonData.from;
this.to = buttonData.to;
this.width = buttonData.width;
this.height = buttonData.height;
this.fill = buttonData.fill;
this.label = buttonData.label;
this.delay = buttonData.delay;
this.textual = buttonData.textual;
this.animation = buttonData.animation;
this.strokeWidth = buttonData.strokeWidth;
};
}
};
/*
*
* Allows the user to modify the look of the widget control bar
* layoutOptions
* .shape - "radial" (only one option for now, will add more soon)
.drawBackground - true/false (if set to true, displays the semi transparent background)
.drawGroupBoundaries - true/false (if set to true, displays the sector like boundaries around button groups)
.drawSpokes - true/false (if set to true, displays the spokes from center to each widget element)
.drawSquareButton - true/false (Not yet implemented)
*/
SAGE2WidgetControl.prototype.setLayoutOptions = function(layoutOptions) {
if (layoutOptions.drawBackground) {
this.layoutOptions.drawBackground = layoutOptions.drawBackground;
}
if ((layoutOptions.drawGroupBoundaries === true) && (layoutOptions.drawBackground === false)) {
this.layoutOptions.drawGroupBoundaries = layoutOptions.drawGroupBoundaries;
}
if (layoutOptions.shape) {
this.layoutOptions.shape = layoutOptions.shape;
}
if (layoutOptions.drawSpokes) {
this.layoutOptions.drawSpokes = layoutOptions.drawSpokes;
}
if (layoutOptions.drawSquareButtons) {
this.layoutOptions.drawSquareButtons = layoutOptions.drawSquareButtons;
}
};
/*
* Adds a button specification
* data
* .type - one of the several predefined button(cover) types [ex: "next", "prev", and so on]
* .action - callback function to specify action after the button has been pressed
* action callback looks like this:
* function (appHandle, date){
* //use the appHandle to perform button click related action here
* }
*/
SAGE2WidgetControl.prototype.addButton = function(data) {
var type = null;
if (this.itemCount <= 30) {
var button = new this.ButtonClass();
button.appId = this.id;
if (data.identifier !== undefined && data.identifier !== null) {
button.id = "button" + data.identifier;
} else {
button.id = "button" + ((this.itemCount < 10) ? "0" : "") + this.itemCount;
}
if (data.hasOwnProperty("label") && data.label !== undefined && data.label !== null) {
type = new this.buttonType.default();
type.label = data.label;
} else if (data.hasOwnProperty("type") && data.type !== undefined && data.type !== null) {
if (typeof data.type === "string") {
var TypeVar = this.buttonType[data.type];
if (typeof TypeVar === "function") {
type = new TypeVar();
}
} else if (typeof data.type === "function") {
type = new data.type();
} else if (typeof data.type === "object") {
var TypeFunc = function() {
this.state = data.type.state;
this.from = data.type.from;
this.to = data.type.to;
this.width = data.type.width;
this.height = data.type.height;
this.fill = data.type.fill;
this.label = data.type.label;
this.strokeWidth = data.type.strokeWidth;
this.delay = data.type.delay;
this.textual = data.type.textual;
this.animation = data.type.animation;
};
type = new TypeFunc();
}
}
if (type === null || type === undefined) {
type = new this.buttonType.default();
}
if (data.initialState !== null && data.initialState !== undefined) {
type.state = data.initialState % 2; // Making sure initial state is 0 or 1
}
button.type = type;
button.width = 1.5 * ui.widgetControlSize;
if (data.hasOwnProperty("position") && data.position !== undefined && data.position !== null) {
this.buttonSequence[data.position.toString()] = button;
} else {
for (var pos = 1; pos <= 30; pos++) {
if (this.buttonSequence.hasOwnProperty(pos.toString()) === false) {
this.buttonSequence[pos] = button;
break;
}
}
}
this.itemCount++;
}
return type;
};
SAGE2WidgetControl.prototype.addSeparatorAfterButtons = function(firstSeparator, secondSeparator, thirdSeparator) {
};
/*
* Adds a text-input bar specification
* data
* .action - callback function to specify action after the text has been input and enter key pressed
* action callback looks like this:
* function (appHandle, text){
* // text contains the string from the text-input widget
* // use the appHandle to send text to the app
* }
*/
SAGE2WidgetControl.prototype.addTextInput = function(data) {
if (this.sideBarElements.length < 5) {
var textInput = new this.TextInputClass();
if (data.identifier !== undefined && data.identifier !== null) {
textInput.id = "textInput" + data.identifier;
} else {
textInput.id = "textInput" + ((this.itemCount < 10) ? "0" : "") + this.itemCount;
}
textInput.appId = this.id;
textInput.label = data.label || null;
textInput.width = 13.0 * ui.widgetControlSize;
textInput.value = data.value || "";
this.sideBarElements[this.sideBarElements.length] = textInput;
this.itemCount++;
} else {
console.log("Can't create widget " + data.identifier + ", no space on widget bar!");
}
};
/*
* Adds a slider specification
* data
* .appHandle
* .property - appHandle and preperty are used to bind a property of the app to the slider
* for example, if you want to bind this.state.currentPage to the slider, then send appHandle:this,
* property:"state.currentPage"
* .begin - the minimum value that the proerty will take
* .end - the maximum value the property will take
* .increments - step value for the proerty
* alternatively, you can specify .parts - number of increments/step values between .begin and .end
*/
SAGE2WidgetControl.prototype.addSlider = function(data) {
// begin,parts,end,action, property, appHandle
if (this.sideBarElements.length < 5) {
var slider = new this.SliderClass();
if (data.identifier !== undefined && data.identifier !== null) {
slider.id = "slider" + data.identifier;
} else {
slider.id = "slider" + ((this.itemCount < 10) ? "0" : "") + this.itemCount;
}
slider.appId = this.id;
slider.begin = data.minimum;
slider.end = data.maximum;
if (data.increments) {
slider.increments = data.increments || 1;
slider.steps = (slider.end - slider.begin) / slider.increments;
} else {
slider.steps = data.steps || 100;
slider.increments = (slider.end - slider.begin) / slider.steps;
}
slider.label = data.label || null;
slider.appProperty = data.property;
slider.sliderVal = data.minimum;
slider.knobLabelFormatFunction = data.labelFormatFunction;
slider.width = 13.0 * ui.widgetControlSize;
if (slider.steps < 1) {
return;
}
this.sideBarElements[this.sideBarElements.length] = slider;
this.itemCount++;
} else {
console.log("Can't create widget " + data.identifier + ", no space on widget bar!");
}
};
/*
* Adds a color palette
*/
SAGE2WidgetControl.prototype.addColorPalette = function(data) {
if (this.hasColorPalette === false && this.itemCount <= 12) {
var colorPalette = new this.ColorPaletteClass();
colorPalette.id = "colorPalette" + this.itemCount;
colorPalette.appId = this.id;
colorPalette.call = data.action || null;
if (data.colorList === null || data.colorList === undefined) {
return;
}
if (data.colorList.length === 0) {
return;
}
this.hasColorPalette = true;
this.colorPalette = colorPalette;
this.itemCount++;
}
};
/*
* Computes the dimensions of the widget control bar
*/
SAGE2WidgetControl.prototype.computeSize = function() {
var size = {
width: 0,
height: 0
};
var dimensions = {};
dimensions.buttonRadius = 0.8 * ui.widgetControlSize;
dimensions.radius = dimensions.buttonRadius * 5.027; // tan(78.5): angle subtended at the center is 22.5
dimensions.firstRadius = dimensions.radius * 0.75;
dimensions.innerR = dimensions.radius - dimensions.buttonRadius - 3; // for the pie slice
dimensions.outerR = ui.widgetControlSize * 6.0;
dimensions.secondRadius = dimensions.firstRadius + dimensions.buttonRadius * 2.5;
size.height = dimensions.outerR * 2.4; // 10% extra on all sides
size.width = size.height;
size.hasSideBar = false;
if (this.sideBarElements.length > 0) {
size.hasSideBar = true;
var elementWidth = Math.max.apply(null, this.sideBarElements.map(function(d) {
return d.width;
}));
size.width = size.width + elementWidth + dimensions.buttonRadius;
var center = {x: size.height / 2.0, y: size.height / 2.0};
var sideBarHeightInAngles = 16;
var theta = sideBarHeightInAngles * this.sideBarElements.length;
var start = 360 - theta / 2;
var point1 = polarToCartesian(dimensions.outerR, start, center);
var point2 = polarToCartesian(dimensions.outerR, start + theta, center);
size.barHeight = Math.abs(point2.y - point1.y);
}
this.controlDimensions = dimensions;
return size;
};
/*
* Creates default close and radial menu buttons
*/
SAGE2WidgetControl.prototype.addDefaultButtons = function(data) {
this.addButton({type: "closeApp", identifier: "CloseApp", position: data.sequence.closeApp});
this.addButton({type: "closeBar", identifier: "CloseWidget", position: data.sequence.closeBar});
};
SAGE2WidgetControl.prototype.addRadioButton = function(data) {
if (this.sideBarElements.length < 5) {
data.options = data.options || [];
if (data.options.length < 2) {
console.log("Radio button should have more than one option!");
return;
}
var radioButton = new this.RadioButton();
if (data.identifier !== undefined && data.identifier !== null) {
radioButton.id = "button_radio" + data.identifier;
} else {
radioButton.id = "button_radio" + ((this.itemCount < 10) ? "0" : "") + this.itemCount;
}
radioButton.appId = this.id;
radioButton.label = data.label || null;
radioButton.data.value = data.options[0];
for (var i = 0; i < data.options.length; i++) {
radioButton.data.options[i] = data.options[i];
if (data.default === data.options[i]) {
radioButton.data.value = data.default;
}
}
radioButton.width = 13.0 * ui.widgetControlSize;
this.sideBarElements[this.sideBarElements.length] = radioButton;
this.itemCount++;
} else {
console.log("Can't create widget " + data.identifier + ", no space on widget bar!");
}
return radioButton.data;
};