src/node-radialmenu.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
/**
* Radial menu for a given pointer
*
* @module server
* @submodule radialmenu
*/
"use strict";
// unused: var radialMenuCenter = { x: 210, y: 210 }; // scale applied in ctor
var radialMenuDefaultSize = { x: 425, y: 425 }; // scale applied in ctor
var thumbnailWindowDefaultSize = { x: 1224, y: 860 };
/**
* Class RadialMenu
*
* @class RadialMenu
* @constructor
*/
function RadialMenu(id, ptrID, config) {
this.id = id;
this.pointerid = ptrID;
this.label = "";
this.color = [255, 255, 255];
this.left = 0; // left/top is the center of the radial menu, NOT the upper left
this.top = 0;
this.visible = true;
this.wsio = undefined;
// Default
this.radialMenuScale = config.ui.widgetControlSize * 0.03;
this.minimumMenuRadiusMeters = 0.1; // 5 cm
this.maximumMenuRadiusMeters;
if (config.ui.auto_scale_ui) {
// this.radialMenuScale = 1;
/*
var borderLeft, borderRight, borderBottom, borderTop;
var tileBorders = config.dimensions.tile_borders;
if (tileBorders) {
borderLeft = parseFloat(tileBorders.left) || 0.0;
borderRight = parseFloat(tileBorders.right) || 0.0;
borderBottom = parseFloat(tileBorders.bottom) || 0.0;
borderTop = parseFloat(tileBorders.top) || 0.0;
} else {
borderLeft = 0.0;
borderRight = 0.0;
borderBottom = 0.0;
borderTop = 0.0;
}
var pixelsPerMeter = config.resolution.width / (config.dimensions.tile_width - borderLeft - borderRight);
var windowDefaultHeightMeters = thumbnailWindowDefaultSize.y / pixelsPerMeter;
// https://en.wikipedia.org/wiki/Optimum_HDTV_viewing_distance#Human_visual_system_limitation
var width = config.layout.columns * (config.dimensions.tile_width + borderLeft + borderRight);
var height = config.layout.rows * (config.dimensions.tile_height + borderBottom + borderTop);
var totalWallDimensionsMeters = { w: width, h: height };
var wallDiagonal = Math.sqrt(Math.pow(totalWallDimensionsMeters.w, 2) + Math.pow(totalWallDimensionsMeters.h, 2));
var DRC = Math.sqrt(Math.pow(totalWallDimensionsMeters.w / totalWallDimensionsMeters.h, 2) + 1);
var calculatedIdealViewingDistance = wallDiagonal / (DRC * thumbnailWindowDefaultSize.y * Math.tan(Math.PI / 180 / 60));
var viewDistRatio = config.layout.rows * (config.dimensions.tile_height + borderBottom + borderTop);
if (config.ui.calculate_viewing_distance) {
viewDistRatio = calculatedIdealViewingDistance / windowDefaultHeightMeters;
console.log("node-radialMenu: calculatedIdealViewingDistance = " + calculatedIdealViewingDistance);
this.radialMenuScale = calculatedIdealViewingDistance * (0.03 * viewDistRatio);
} else {
viewDistRatio = config.dimensions.viewing_distance / windowDefaultHeightMeters;
this.radialMenuScale = config.dimensions.viewing_distance * (0.03 * viewDistRatio);
}
var radialMenuRadiusMeters = radialMenuDefaultSize.x * this.radialMenuScale / pixelsPerMeter;
// Set radial menu radius bounds
if (radialMenuRadiusMeters < (2 * this.minimumMenuRadiusMeters)) { // lower
this.radialMenuScale = 2 * this.minimumMenuRadiusMeters / radialMenuDefaultSize.x * pixelsPerMeter;
}
var totalContentWindowSize = {
w: (radialMenuDefaultSize.x + thumbnailWindowDefaultSize.x) * this.radialMenuScale / pixelsPerMeter,
h: thumbnailWindowDefaultSize.y * this.radialMenuScale / pixelsPerMeter };
// Radial menu + thumbnail window can never be more than 90% of the display width or height
if (totalContentWindowSize.w > totalWallDimensionsMeters.w) {
this.radialMenuScale = totalWallDimensionsMeters.w * 0.9 / (radialMenuDefaultSize.x +
thumbnailWindowDefaultSize.x) * pixelsPerMeter;
}
// Recalculate size
totalContentWindowSize = {
w: (radialMenuDefaultSize.x + thumbnailWindowDefaultSize.x) * this.radialMenuScale / pixelsPerMeter,
h: (thumbnailWindowDefaultSize.y + 100) * this.radialMenuScale / pixelsPerMeter };
if (totalContentWindowSize.h > totalWallDimensionsMeters.h) {
this.radialMenuScale = totalWallDimensionsMeters.h * 0.9 / thumbnailWindowDefaultSize.y * pixelsPerMeter;
}
*/
console.log("node-radialMenu: this.radialMenuScale = " + this.radialMenuScale);
}
this.radialMenuSize = {
x: radialMenuDefaultSize.x * this.radialMenuScale,
y: radialMenuDefaultSize.y * this.radialMenuScale
};
this.thumbnailWindowSize = {
x: thumbnailWindowDefaultSize.x * this.radialMenuScale,
y: thumbnailWindowDefaultSize.y * this.radialMenuScale
};
this.activeEventIDs = [];
this.dragState = false;
this.dragID = -1;
this.dragPosition = { x: 0, y: 0 };
// States
this.thumbnailWindowState = "closed"; // closed, images, pdfs, videos, applauncher, sessions
this.thumbnailWindowScrollPosition = 0;
this.buttonStates = {}; // idle, lit, over for every radial menu button
this.radialButtons = {};
this.buttonAngle = 36; // Degrees of separation between each radial button position
this.menuButtonSize = 100;
this.menuRadius = 95;
this.pointersOnMenu = {}; // Stores the pointerIDs that are on the menu, but not on a button
this.showArrangementSubmenu = false;
// id - unique button id
// icon - button icon
// radialPosition - 0 = top of menu, 1 = buttonAngle degrees clockwise, 2 = buttonAngle*2 degrees clockwise, etc.
this.radialButtons.closeMenu = {id: 7, icon: "images/ui/close.svg", radialPosition: 7.5, radialLevel: 0,
group: "radialMenu", action: "close", window: "radialMenu", state: 0, pointers: {} };
this.radialButtons.images = {id: 0, icon: "images/ui/images.svg", radialPosition: 0, radialLevel: 1,
group: "radialMenu", action: "contentWindow", window: "image", state: 0, pointers: {} };
this.radialButtons.pdfs = {id: 1, icon: "images/ui/pdfs.svg", radialPosition: 1, radialLevel: 1,
group: "radialMenu", action: "contentWindow", window: "pdf", state: 0, pointers: {} };
this.radialButtons.videos = {id: 2, icon: "images/ui/videos.svg", radialPosition: 2, radialLevel: 1,
group: "radialMenu", action: "contentWindow", window: "video", state: 0, pointers: {} };
this.radialButtons.apps = {id: 3, icon: "images/ui/applauncher.svg", radialPosition: 3, radialLevel: 1,
group: "radialMenu", action: "contentWindow", window: "applauncher", state: 0, pointers: {} };
this.radialButtons.loadSession = {id: 4, icon: "images/ui/loadsession.svg", radialPosition: 4, radialLevel: 1,
group: "radialMenu", action: "contentWindow", window: "session", state: 0, pointers: {} };
// Arrangement submenu
this.radialButtons.settings = {id: 6, icon: "images/ui/arrangement.svg", radialPosition: 6.5, radialLevel: 1,
group: "radialMenu", action: "toggleSubRadial", radial: "settingsMenu", state: 0, pointers: {} };
this.radialButtons.tileContent = {id: 8, icon: "images/ui/tilecontent.svg", radialPosition: 6.5, radialLevel: 2,
group: "settingsMenu", action: "tileContent", state: 0, pointers: {} };
this.radialButtons.clearContent = {id: 9, icon: "images/ui/clearcontent.svg", radialPosition: 7.1, radialLevel: 2,
group: "settingsMenu", action: "clearAllContent", state: 0, pointers: {} };
this.radialButtons.saveSession = {id: 5, icon: "images/ui/savesession.svg", radialPosition: 5.9, radialLevel: 2,
group: "settingsMenu", action: "saveSession", state: 0, pointers: {} };
}
/**
* Adds geometry to the Interaction module
*
* @method generateGeometry
* @param interactMgr Interaction manager
*/
RadialMenu.prototype.generateGeometry = function(interactMgr, radialMenus) {
this.interactMgr = interactMgr;
this.interactMgr.addGeometry(this.id + "_menu_radial", "radialMenus", "circle",
{ x: this.left, y: this.top, r: this.radialMenuSize.y / 2},
true, Object.keys(radialMenus).length, this);
this.interactMgr.addGeometry(this.id + "_menu_thumbnail", "radialMenus", "rectangle",
{x: this.left, y: this.top, w: this.thumbnailWindowSize.x, h: this.thumbnailWindowSize.y},
false, Object.keys(radialMenus).length, this);
for (var buttonName in this.radialButtons) {
var buttonInfo = this.radialButtons[buttonName];
var buttonRadius = 25 * this.radialMenuScale;
var buttonRadialDistance = this.menuRadius;
if (buttonInfo.radialLevel == 2) {
buttonRadialDistance = this.menuRadius * 1.6;
}
var angle = (90 + this.buttonAngle * buttonInfo.radialPosition) * (Math.PI / 180);
var position = {
x: this.left - (buttonRadialDistance - buttonRadius / 2) * this.radialMenuScale * Math.cos(angle),
y: this.top - (buttonRadialDistance - buttonRadius / 2) * this.radialMenuScale * Math.sin(angle)
};
var visible = true;
if (buttonInfo.radialLevel === 0) {
position = {
x: this.left - (0 - buttonRadius / 2) * this.radialMenuScale * Math.cos(angle),
y: this.top - (0 - buttonRadius / 2) * this.radialMenuScale * Math.sin(angle)
};
} else if (buttonInfo.radialLevel !== 1) {
// visible = false;
}
this.interactMgr.addGeometry(this.id + "_menu_radial_button_" + buttonName, "radialMenus", "circle",
{x: position.x, y: position.y, r: buttonRadius}, visible, Object.keys(radialMenus).length + 1, this);
}
};
/**
* Returns information on the radial menu's layout and position
*
* @method getInfo
*/
RadialMenu.prototype.getInfo = function() {
return {
id: this.pointerid,
x: this.left,
y: this.top,
radialMenuSize: this.radialMenuSize,
thumbnailWindowSize: this.thumbnailWindowSize,
radialMenuScale: this.radialMenuScale,
visible: this.visible,
layout: this.radialButtons,
thumbnailWindowState: this.thumbnailWindowState,
arrangementMenuState: this.showArrangementSubmenu
};
};
/**
*
*
* @method onButtonEvent
* @param buttonID
* @param pointerID
* @return stateChange -1 = no change, 0 = now idle, 1 = now mouse over, 2 = now clicked
*/
RadialMenu.prototype.onButtonEvent = function(buttonID, pointerID, buttonType, color) {
var buttonName = buttonID.substring((this.id + "_menu_radial_button_").length, buttonID.length);
var action;
var otherButtonName;
if (buttonType === "pointerPress") {
// Process based on button type
// console.log("node-radialMenu: button press on " + buttonName);
if (this.radialButtons[buttonName].action === "contentWindow") { // Actions with parameters
// Set thumbnail window and button lit state
if (this.thumbnailWindowState === this.radialButtons[buttonName].window) {
this.thumbnailWindowState = "closed"; // Mark the content window to be closed
this.radialButtons[buttonName].state = 0; // Dim the current button
} else {
this.thumbnailWindowState = this.radialButtons[buttonName].window;
this.radialButtons[buttonName].state = 5; // Highlight the current button
}
// Clear button lit state for other buttonState
for (otherButtonName in this.radialButtons) {
if (otherButtonName !== buttonName) {
// console.log("Clear button state for " + otherButtonName);
delete this.radialButtons[otherButtonName].pointers[pointerID];
if (Object.keys(this.radialButtons[otherButtonName].pointers).length === 0) {
if (this.radialButtons[otherButtonName].state !== 0 &&
this.thumbnailWindowState !== this.radialButtons[otherButtonName].window) {
this.radialButtons[otherButtonName].state = 0;
}
}
this.buttonStates[otherButtonName] = this.radialButtons[otherButtonName].state;
}
}
// Set the visibility of the content window
this.interactMgr.editVisibility(this.id + "_menu_thumbnail", "radialMenus", (this.thumbnailWindowState !== "closed"));
action = {type: this.radialButtons[buttonName].action, window: this.radialButtons[buttonName].window};
} else if (this.radialButtons[buttonName].action === "toggleSubRadial") { // Actions with parameters
// Radial submenus
action = {type: this.radialButtons[buttonName].action, window: this.radialButtons[buttonName].radial};
this.showArrangementSubmenu = !this.showArrangementSubmenu;
} else { // All no parameter actions
action = {type: this.radialButtons[buttonName].action};
// Close button
if (action.type === "close") {
this.hide();
}
if (this.showArrangementSubmenu === false) {
// Save session button
if (action.type === "saveSession") {
// NOTE: This action is handled by the server radialMenuEvent()
action.type = "none";
}
// Tile content
if (action.type === "tileContent") {
// NOTE: This action is handled by the server radialMenuEvent()
action.type = "none";
}
// Clear all content button
if (action.type === "clearAllContent") {
// NOTE: This action is handled by the server radialMenuEvent()
action.type = "none";
}
}
}
}
// Update the menu state
this.buttonStates[buttonName] = this.radialButtons[buttonName].state;
return {action: action, buttonState: this.buttonStates, color: color};
};
/**
*
*
* @method onMenuEvent
* @param pointerID
* @return stateChange
*/
RadialMenu.prototype.onMenuEvent = function(pointerID) {
if (pointerID in this.pointersOnMenu) {
// console.log("Existing pointer event on menu: "+pointerID);
} else {
// console.log("New pointer event on menu: "+pointerID);
this.pointersOnMenu[pointerID] = "";
// Clear this pointer ID from all buttons (clear hover state)
var buttonStates = {};
for (var buttonName in this.radialButtons) {
delete this.radialButtons[buttonName].pointers[pointerID];
if (Object.keys(this.radialButtons[buttonName].pointers).length === 0) {
if (this.radialButtons[buttonName].state !== 0 && this.radialButtons[buttonName].state !== 5) {
this.radialButtons[buttonName].state = 0;
}
}
buttonStates[buttonName] = this.radialButtons[buttonName].state;
}
return {buttonState: buttonStates};
}
};
/**
* Gets the short name of a button given the long name
*
* @method getShortButtonName
* @param longButtonName
*/
RadialMenu.prototype.getShortButtonName = function(longName) {
var buttonName = longName.substring((this.id + "_menu_radial_button_").length, longName.length);
return buttonName;
};
/**
* Returns if the thumbnail window is currently open
*
* @method isThumbnailWindowOpen
* @return openState
*/
RadialMenu.prototype.isThumbnailWindowOpen = function() {
return this.thumbnailWindowState !== "closed";
};
/**
*
*
* @method setScale
*/
RadialMenu.prototype.setScale = function(value) {
this.radialMenuScale = value / 100;
this.radialMenuSize = {
x: radialMenuDefaultSize.x * this.radialMenuScale,
y: radialMenuDefaultSize.y * this.radialMenuScale
};
this.thumbnailWindowSize = {
x: thumbnailWindowDefaultSize.x * this.radialMenuScale,
y: thumbnailWindowDefaultSize.y * this.radialMenuScale
};
};
/**
*
*
* @method show
*/
RadialMenu.prototype.show = function() {
this.visible = true;
};
/**
*
*
* @method hide
*/
RadialMenu.prototype.hide = function() {
this.visible = false;
this.interactMgr.editVisibility(this.id + "_menu_radial", "radialMenus", false);
this.interactMgr.editVisibility(this.id + "_menu_thumbnail", "radialMenus", false);
for (var buttonName in this.radialButtons) {
this.interactMgr.editVisibility(this.id + "_menu_radial_button_" + buttonName, "radialMenus", false);
this.radialButtons[buttonName].state = 0;
}
this.thumbnailWindowState = "closed";
};
/**
*
*
* @method setPosition
*/
RadialMenu.prototype.setPosition = function(data) {
this.show();
this.left = data.x;
this.top = data.y;
this.interactMgr.editGeometry(this.id + "_menu_radial", "radialMenus", "circle", {
x: this.left, y: this.top, r: this.radialMenuSize.y / 2
});
this.interactMgr.editGeometry(this.id + "_menu_thumbnail", "radialMenus", "rectangle", {
x: this.getThumbnailWindowPosition().x, y: this.getThumbnailWindowPosition().y,
w: this.thumbnailWindowSize.x, h: this.thumbnailWindowSize.y
});
for (var buttonName in this.radialButtons) {
var buttonInfo = this.radialButtons[buttonName];
var buttonRadius = this.menuButtonSize / 4 * this.radialMenuScale;
var angle = (90 + this.buttonAngle * buttonInfo.radialPosition) * (Math.PI / 180);
var position = {
x: this.left - buttonRadius - this.menuRadius * this.radialMenuScale * Math.cos(angle),
y: this.top - buttonRadius - this.menuRadius * this.radialMenuScale * Math.sin(angle)
};
if (buttonInfo.radialLevel === 0) {
position = {
x: this.left - buttonRadius,
y: this.top - buttonRadius
};
} else if (buttonInfo.radialLevel === 2) {
position = {
x: this.left - buttonRadius - (this.menuRadius * 1.6) * this.radialMenuScale * Math.cos(angle),
y: this.top - buttonRadius - (this.menuRadius * 1.6) * this.radialMenuScale * Math.sin(angle)
};
}
// console.log("setPosition: " + buttonName + " " +menuRadius * Math.cos(angle) + " " + menuRadius * Math.sin(angle) );
this.interactMgr.editGeometry(this.id + "_menu_radial_button_" + buttonName, "radialMenus",
"rectangle", {x: position.x, y: position.y, w: buttonRadius * 2, h: buttonRadius * 2});
this.interactMgr.editVisibility(this.id + "_menu_radial_button_" + buttonName, "radialMenus", true);
}
// console.log("done");
};
/**
*
*
* @method getThumbnailWindowPosition
*/
RadialMenu.prototype.getThumbnailWindowPosition = function() {
return { x: this.left + this.radialMenuSize.x / 2, y: this.top - this.radialMenuSize.y / 2};
};
/**
*
*
* @method hasEventID
*/
RadialMenu.prototype.hasEventID = function(id) {
if (this.activeEventIDs.indexOf(id) === -1) {
return false;
}
return true;
};
/**
*
*
* @method isEventOnMenu
*/
RadialMenu.prototype.isEventOnMenu = function(data) {
if (this.visible === true) {
// If over radial menu bounding box
if ((data.x > this.left - this.radialMenuSize.x / 2) &&
(data.x < this.left - this.radialMenuSize.x / 2 + this.radialMenuSize.x) &&
(data.y > this.top - this.radialMenuSize.y / 2) &&
(data.y < this.top - this.radialMenuSize.y / 2 + this.radialMenuSize.y)) {
return true;
}
if ((data.x > this.left + this.radialMenuSize.x / 2) &&
(data.x < this.left + this.radialMenuSize.x / 2 + this.thumbnailWindowSize.x) &&
(data.y > this.top - this.radialMenuSize.y / 2) &&
(data.y < this.top - this.radialMenuSize.y / 2 + this.thumbnailWindowSize.y)) {
// Else if over thumbnail window bounding box
if (this.isThumbnailWindowOpen()) {
return true;
}
}
}
return false;
};
/**
*
*
* @method onEvent
*/
RadialMenu.prototype.onEvent = function(data) {
var idIndex = this.activeEventIDs.indexOf(data.id);
if (idIndex !== -1 && data.type === "pointerRelease") {
this.activeEventIDs.splice(idIndex);
}
if (this.visible === true) {
// Press over radial menu, drag menu
// console.log((this.left - this.radialMenuSize.x / 2), " < ", position.x, " < ",
// (this.left - this.radialMenuSize.x / 2 + this.radialMenuSize.x) );
// console.log((this.top - this.radialMenuSize.y / 2), " < ", position.y, " < ",
// (this.top - this.radialMenuSize.y / 2 + this.radialMenuSize.y) );
// If over radial menu bounding box
if ((data.x > this.left - this.radialMenuSize.x / 2) &&
(data.x < this.left - this.radialMenuSize.x / 2 + this.radialMenuSize.x) &&
(data.y > this.top - this.radialMenuSize.y / 2) &&
(data.y < this.top - this.radialMenuSize.y / 2 + this.radialMenuSize.y)) {
// this.windowInteractionMode = false;
if (this.visible === true && data.type === "pointerPress") {
this.activeEventIDs.push(data.id);
}
return true;
}
if (this.isThumbnailWindowOpen() &&
(data.x > this.left + this.radialMenuSize.x / 2) &&
(data.x < this.left + this.radialMenuSize.x / 2 + this.thumbnailWindowSize.x) &&
(data.y > this.top - this.radialMenuSize.y / 2) &&
(data.y < this.top - this.radialMenuSize.y / 2 + this.thumbnailWindowSize.y)) {
// Else if over thumbnail window bounding box
// this.windowInteractionMode = false;
if (this.visible === true && data.type === "pointerPress") {
this.activeEventIDs.push(data.id);
}
return true;
}
if (this.activeEventIDs.indexOf(data.id) !== -1) {
return true;
}
}
return false;
};
/**
*
*
* @method onPress
*/
RadialMenu.prototype.onPress = function(id) {
this.activeEventIDs.push(id);
};
/**
*
*
* @method onMove
*/
RadialMenu.prototype.onMove = function() {
// console.log( this.hasEventID(id) );
};
/**
*
*
* @method onRelease
*/
RadialMenu.prototype.onRelease = function(id) {
// console.log("node-RadialMenu.onRelease()");
this.activeEventIDs.splice(this.activeEventIDs.indexOf(id), 1);
// console.log("drag state "+ this.dragID + " " + id);
if (this.dragState === true && this.dragID === id) {
this.dragState = false;
}
};
/**
* Initializes the radial menu's drag state
*
* @method onStartDrag
* @param id {Integer} input ID initiating the drag
* @param localPos {x: Float, y: Float} initial drag position
*/
RadialMenu.prototype.onStartDrag = function(id, localPos) {
if (this.dragState === false) {
this.dragID = id;
this.dragState = true;
this.dragPosition = localPos;
}
};
/**
* Checks if an input ID is dragging the menu
*
* @method isDragging
* @param id {Integer} input ID
* @param localPos {x: Float, y: Float} input position
* @return dragPos {x: Float, y: Float}
*/
RadialMenu.prototype.getDragOffset = function(id, localPos) {
var offset = {x: 0, y: 0 };
if (this.dragState === true && this.dragID === id) {
// If this ID is dragging the menu, return the drag offset
offset = { x: localPos.x - this.dragPosition.x, y: localPos.y - this.dragPosition.y };
this.dragPosition = localPos;
}
return offset;
};
module.exports = RadialMenu;