server.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-2015
/**
* SAGE2 server
*
* @class server
* @module server
* @submodule server-core
* @requires fs http https os path readline url formidable gm json5 qr-image sprint websocketio
*/
// node mode
/* jshint node: true */
// how to deal with spaces and tabs
/* jshint smarttabs: false */
// Don't make functions within a loop
/* jshint -W083 */
/*global mediaFolders */
// require variables to be declared
'use strict';
// node: built-in
var fs = require('fs'); // filesystem access
var http = require('http'); // http server
var https = require('https'); // https server
var os = require('os'); // operating system access
var path = require('path'); // file path management
var readline = require('readline'); // to build an evaluation loop
var url = require('url'); // parses urls
// npm: defined in package.json
var formidable = require('formidable'); // upload processor
var gm = require('gm'); // graphicsmagick
var json5 = require('json5'); // Relaxed JSON format
var qrimage = require('qr-image'); // qr-code generation
var sprint = require('sprint'); // pretty formating (sprintf)
var imageMagick; // derived from graphicsmagick
var WebsocketIO = require('websocketio'); // creates WebSocket server and clients
var chalk = require('chalk'); // used for colorizing the console output
var commander = require('commander'); // parsing command-line arguments
// custom node modules
var sageutils = require('./src/node-utils'); // provides the current version number
var assets = require('./src/node-assets'); // manages the list of files
// var commandline = require('./src/node-sage2commandline'); // handles command line parameters for SAGE2
var exiftool = require('./src/node-exiftool'); // gets exif tags for images
var pixelblock = require('./src/node-pixelblock'); // chops pixels buffers into square chunks
var md5 = require('./src/md5'); // return standard md5 hash of given param
var HttpServer = require('./src/node-httpserver'); // creates web server
var InteractableManager = require('./src/node-interactable'); // handles geometry and determining which object a point is over
var Interaction = require('./src/node-interaction'); // handles sage interaction (move, resize, etc.)
var Loader = require('./src/node-itemloader'); // handles sage item creation
var Omicron = require('./src/node-omicron');
var Drawing = require('./src/node-drawing'); // handles Omicron input events
var Radialmenu = require('./src/node-radialmenu'); // radial menu
var Sage2ItemList = require('./src/node-sage2itemlist'); // list of SAGE2 items
var Sagepointer = require('./src/node-sagepointer'); // handles sage pointers (creation, location, etc.)
var StickyItems = require('./src/node-stickyitems');
var registry = require('./src/node-registry'); // Registry Manager
var FileBufferManager = require('./src/node-filebuffer');
var PartitionList = require('./src/node-partitionlist'); // list of SAGE2 Partitions
var SharedDataManager = require('./src/node-sharedserverdata'); // manager for shared data
var userlist = require('./src/node-userlist'); // list of users
var S2Logger = require('./src/node-logger'); // SAGE2 logging module
var PerformanceManager = require('./src/node-performancemanager'); // SAGE2 performance module
var VoiceActionManager = require('./src/node-voiceToAction'); // manager for shared data
//
// Globals
//
// Global variable for all media folders
global.mediaFolders = {};
// System folder, defined within SAGE2 installation
mediaFolders.system = {
name: "system",
path: "public/uploads/",
url: "/uploads",
upload: false
};
// Home directory, defined as ~/Documents/SAGE2_Media or equivalent
mediaFolders.user = {
name: "user",
path: path.join(sageutils.getHomeDirectory(), "Documents", "SAGE2_Media", "/"),
url: "/user",
upload: true
};
// Default upload folder
var mainFolder = mediaFolders.user;
// Session hash for security
global.__SESSION_ID = null;
// SAGE2 logger object
global.logger = null;
var sage2Server = null;
var sage2ServerS = null;
var wsioServer = null;
var wsioServerS = null;
var platform = os.platform() === "win32" ? "Windows" : os.platform() === "darwin" ? "Mac OS X" : "Linux";
var imageMagickOptions = {imageMagick: true};
var ffmpegOptions = {};
var hostOrigin = "";
var SAGE2Items = {};
var sharedApps = {};
var users = null;
var appLoader = null;
var mediaBlockSize = 512;
var pressingCTRL = true;
var fileBufferManager;
var startTime;
var program;
var config;
var SAGE2_version;
var interactMgr;
var drawingManager;
var performanceManager;
// Partition variables
var partitions;
var draggingPartition = {};
var cuttingPartition = {};
// Array containing the remote sites informations (toolbar on top of wall)
var remoteSites = [];
// GO
startTime = Date.now();
// Get the version from package.json
SAGE2_version = sageutils.getShortVersion();
// Parse commond line arguments
program = commander
.version(SAGE2_version)
.option('-i, --no-interactive', 'Non interactive prompt')
.option('-f, --configuration <file>', 'Specify a configuration file')
.option('-l, --logfile [file]', 'Specify a log file')
.option('-q, --no-output', 'Quiet, no output')
.option('-s, --session [name]', 'Load a session file (last session if omitted)')
.option('-t, --track-users [file]', 'enable user interaction tracking (specified file indicates users to track)')
.option('-p, --password <password>', 'Sets the password to connect to SAGE2 session')
.parse(process.argv);
// Logging or not
if (program.logfile) {
// Use default name or one specified on command line
var logname = (program.logfile === true) ? 'sage2.log' : program.logfile;
// Create the loggger object
global.logger = new S2Logger({name: "server", path: logname});
// Redirect console.log to a file and still produces an output or not
if (program.output === false) {
program.interactive = undefined;
}
}
// Set global variable for the log function
global.quiet = !commander.output;
// Load the configuration file
config = loadConfiguration();
// Export the config variable
global.config = config;
// Create the 'rbush' data structure for interaction calculation
interactMgr = new InteractableManager();
// Setup the partition data structure
partitions = new PartitionList(config);
// FileBufferManager, I guess
fileBufferManager = new FileBufferManager();
// Create structure to handle automated placement of apps
var appLaunchPositioning = {
xStart: 10,
yStart: 50,
xLast: -1,
yLast: -1,
widthLast: -1,
heightLast: -1,
tallestInRow: -1,
padding: 20
};
// Add extra folders defined in the configuration file
if (config.folders) {
config.folders.forEach(function(f) {
// Add a new folder into the collection
mediaFolders[f.name] = {};
mediaFolders[f.name].name = f.name;
mediaFolders[f.name].path = f.path;
mediaFolders[f.name].url = f.url;
mediaFolders[f.name].upload = sageutils.isTrue(f.upload);
});
}
var publicDirectory = "public";
var uploadsDirectory = path.join(publicDirectory, "uploads");
var sessionDirectory = path.join(publicDirectory, "sessions");
var whiteboardDirectory = sessionDirectory;
// Validate all the media folders
for (var folder in mediaFolders) {
var f = mediaFolders[folder];
sageutils.log("Folders", '[' + f.name + ']',
'path:', chalk.yellow.bold(f.path),
'url:', chalk.yellow.bold(f.url));
if (!sageutils.folderExists(f.path)) {
sageutils.mkdirParent(f.path);
}
if (mediaFolders[f.name].upload) {
mediaFolders.system.upload = false;
// Update the main upload folder
uploadsDirectory = f.path;
mainFolder = f;
sessionDirectory = path.join(uploadsDirectory, "sessions");
whiteboardDirectory = path.join(uploadsDirectory, "whiteboard");
if (!sageutils.folderExists(sessionDirectory)) {
sageutils.mkdirParent(sessionDirectory);
}
sageutils.log("Folders", 'upload to', chalk.yellow.bold(f.path));
}
var newdirs = ["apps", "assets", "images", "pdfs",
"tmp", "videos", "config", "whiteboard", "web"];
newdirs.forEach(function(d) {
var newsubdir = path.join(mediaFolders[f.name].path, d);
if (!sageutils.folderExists(newsubdir)) {
sageutils.mkdirParent(newsubdir);
}
});
}
// Add back all the media folders to the configuration structure
config.folders = mediaFolders;
sageutils.log("SAGE2", chalk.cyan("Node Version:\t\t"),
chalk.green.bold(sageutils.getNodeVersion()));
sageutils.log("SAGE2", chalk.cyan("Detected Server OS as:\t"),
chalk.green.bold(platform));
sageutils.log("SAGE2", chalk.cyan("SAGE2 Short Version:\t"),
chalk.green.bold(SAGE2_version));
// Initialize Server
initializeSage2Server();
/**
* initialize the SAGE2 server
*
* @method initializeSage2Server
*/
function initializeSage2Server() {
// Remove API keys from being investigated further
// if (config.apis) delete config.apis;
// Register with evl's server
if (config.register_site) {
sageutils.registerSAGE2(config);
}
// Check for missing packages
// pass parameter `true` for devel packages also
if (process.arch !== 'arm') {
// seems very slow to do on ARM processor (Raspberry PI)
sageutils.checkPackages();
}
// Setup binaries path
if (config.dependencies !== undefined) {
if (config.dependencies.ImageMagick !== undefined) {
imageMagickOptions.appPath = config.dependencies.ImageMagick;
}
if (config.dependencies.FFMpeg !== undefined) {
ffmpegOptions.appPath = config.dependencies.FFMpeg;
}
}
// Create an object to gather performance statistics
performanceManager = new PerformanceManager();
performanceManager.initializeConfiguration(config);
performanceManager.wrapDataTransferFunctions(WebsocketIO);
imageMagick = gm.subClass(imageMagickOptions);
assets.initializeConfiguration(config);
assets.setupBinaries(imageMagickOptions, ffmpegOptions);
// Set default host origin for this server
if (config.rproxy_port === undefined) {
hostOrigin = "http://" + config.host + (config.port === 80 ? "" : ":" + config.port) + "/";
}
// Initialize sage2 item lists
SAGE2Items.applications = new Sage2ItemList();
SAGE2Items.portals = new Sage2ItemList();
SAGE2Items.pointers = new Sage2ItemList();
SAGE2Items.radialMenus = new Sage2ItemList();
SAGE2Items.widgets = new Sage2ItemList();
SAGE2Items.renderSync = {};
SAGE2Items.portals.interactMgr = {};
// Initialize user interaction tracking
if (program.trackUsers) {
if (typeof program.trackUsers === "string" && sageutils.fileExists(program.trackUsers)) {
users = json5.parse(fs.readFileSync(program.trackUsers));
} else {
users = {};
}
users.session = {};
users.session.start = Date.now();
setInterval(saveUserLog, 300000); // every 5 minutes
if (!sageutils.folderExists("logs")) {
fs.mkdirSync("logs");
}
}
// Generate a qr image that points to sage2 server
var qr_png = qrimage.image(hostOrigin, { ec_level: 'M', size: 15, margin: 3, type: 'png' });
var qr_out = path.join(uploadsDirectory, "images", "QR.png");
qr_png.on('end', function() {
sageutils.log("QR", "QR code generated", qr_out);
});
qr_png.pipe(fs.createWriteStream(qr_out));
// Setup tmp directory for SAGE2 server
process.env.TMPDIR = path.join(__dirname, "tmp");
sageutils.log("SAGE2", "Temp folder:", chalk.yellow.bold(process.env.TMPDIR));
if (!sageutils.folderExists(process.env.TMPDIR)) {
fs.mkdirSync(process.env.TMPDIR);
}
// Setup tmp directory in uploads
var uploadTemp = path.join(__dirname, "public", "uploads", "tmp");
sageutils.log("SAGE2", "Upload temp folder:", chalk.yellow.bold(uploadTemp));
if (!sageutils.folderExists(uploadTemp)) {
fs.mkdirSync(uploadTemp);
}
// Make sure sessions directory exists
if (!sageutils.folderExists(sessionDirectory)) {
fs.mkdirSync(sessionDirectory);
}
// Add a flag into the configuration to denote password status (used on display side)
// not protected by default
config.passwordProtected = false;
// Check for the session password file
var userDocPath = path.join(sageutils.getHomeDirectory(), "Documents", "SAGE2_Media", "/");
var passwordFile = userDocPath + 'passwd.json';
if (typeof program.password === "string" && program.password.length > 0) {
// Creating a new hash from the password
global.__SESSION_ID = md5.getHash(program.password);
sageutils.log("Secure", "Using", global.__SESSION_ID, "as the key for this session");
// Saving the hash
fs.writeFileSync(passwordFile, JSON.stringify({pwd: global.__SESSION_ID}));
sageutils.log("Secure", "Saved to file name", passwordFile);
// the session is protected
config.passwordProtected = true;
} else if (sageutils.fileExists(passwordFile)) {
// If a password file exists, load it
var passwordFileJsonString = fs.readFileSync(passwordFile, 'utf8');
var passwordFileJson = JSON.parse(passwordFileJsonString);
if (passwordFileJson.pwd !== null) {
global.__SESSION_ID = passwordFileJson.pwd;
sageutils.log("Secure", "A sessionID was found:", passwordFileJson.pwd);
// the session is protected
config.passwordProtected = true;
} else {
sageutils.log("Secure", "Invalid hash file", passwordFile);
}
}
/*
Monitor OFF, cause issues with drag-drop files
// Monitoring some folders
var listOfFolders = [];
for (var lf in mediaFolders) {
listOfFolders.push(mediaFolders[lf].path);
}
// try to exclude some folders from the monitoring
var excludesFiles = ['.DS_Store', 'Thumbs.db', 'passwd.json'];
var excludesFolders = ['assets', 'apps', 'config', 'savedFiles', 'sessions', 'tmp'];
sageutils.monitorFolders(listOfFolders, excludesFiles, excludesFolders,
function(change) {
sageutils.log("Monitor", "Changes detected in", this.root);
if (change.modifiedFiles.length > 0) {
sageutils.log("Monitor", " Modified files: %j", change.modifiedFiles);
// broadcast the new file list
// assets.refresh(this.root, function(count) {
// broadcast('storedFileList', getSavedFilesList());
// });
}
if (change.addedFiles.length > 0) {
// sageutils.log("Monitor", " Added files: %j", change.addedFiles);
}
if (change.removedFiles.length > 0) {
// sageutils.log("Monitor", " Removed files: %j", change.removedFiles);
}
if (change.addedFolders.length > 0) {
// sageutils.log("Monitor", " Added folders: %j", change.addedFolders);
}
if (change.modifiedFolders.length > 0) {
// sageutils.log("Monitor", " Modified folders: %j", change.modifiedFolders);
}
if (change.removedFolders.length > 0) {
// sageutils.log("Monitor", " Removed folders: %j", change.removedFolders);
}
}
);
*/
// Initialize app loader
appLoader = new Loader(mainFolder.path, hostOrigin, config, imageMagickOptions, ffmpegOptions);
// Initialize interactable manager and layers
interactMgr.addLayer("staticUI", 3);
interactMgr.addLayer("radialMenus", 2);
interactMgr.addLayer("widgets", 1);
interactMgr.addLayer("applications", 0);
interactMgr.addLayer("portals", 0);
interactMgr.addLayer("partitions", 0);
// Initialize the background for the display clients (image or color)
setupDisplayBackground();
// initialize dialog boxes
setUpDialogsAsInteractableObjects();
// Setup the remote sites for collaboration
initalizeRemoteSites();
// Set up http and https servers
var httpServerApp = new HttpServer(publicDirectory);
httpServerApp.httpPOST('/upload', uploadForm); // receive newly uploaded files from SAGE Pointer / SAGE UI
httpServerApp.httpGET('/config', sendConfig); // send config object to client using http request
var options = setupHttpsOptions(); // create HTTPS options - sets up security keys
sage2Server = http.createServer(httpServerApp.onrequest);
sage2ServerS = https.createServer(options, httpServerApp.onrequest);
// In case the HTTPS client doesnt support tickets
var tlsSessionStore = {};
sage2ServerS.on('newSession', function(id, data, cb) {
tlsSessionStore[id.toString('hex')] = data;
cb();
});
sage2ServerS.on('resumeSession', function(id, cb) {
cb(null, tlsSessionStore[id.toString('hex')] || null);
});
// Set up websocket servers - 2 way communication between server and all browser clients
wsioServer = new WebsocketIO.Server({server: sage2Server});
wsioServerS = new WebsocketIO.Server({server: sage2ServerS});
wsioServer.onconnection(openWebSocketClient);
wsioServerS.onconnection(openWebSocketClient);
// Get full version of SAGE2 - git branch, commit, date
sageutils.getFullVersion(function(version) {
// fields: base commit branch date
SAGE2_version = version;
sageutils.log("SAGE2", "Full Version:", json5.stringify(SAGE2_version));
broadcast('setupSAGE2Version', SAGE2_version);
if (users !== null) {
users.session.version = SAGE2_version;
}
});
// Initialize assets folders
assets.initialize(mainFolder, mediaFolders, function() {
// when processing assets done, send the file list
broadcast('storedFileList', getSavedFilesList());
});
drawingManager = new Drawing(config);
drawingManager.setCallbacks(
drawingInit,
drawingUpdate,
drawingRemove,
sendTouchToPalette,
sendDragToPalette,
sendStyleToPalette,
sendChangeToPalette,
movePaletteTo,
saveDrawingSession,
loadDrawingSession,
sendSessionListToPalette
);
// Link the interactable manager to the drawing manager
drawingManager.linkInteractableManager(interactMgr);
}
/************************Whiteboard Callbacks************************/
function drawingInit(clientWebSocket, drawState) {
clientWebSocket.emit("drawingInit", drawState);
}
function drawingUpdate(clientWebSocket, drawingObject) {
clientWebSocket.emit("drawingUpdate", drawingObject);
}
function drawingRemove(clientWebSocket, drawingObject) {
clientWebSocket.emit("drawingRemove", drawingObject);
}
function sendTouchToPalette(paletteID, x, y) {
var ePosition = {x: x, y: y};
var eUser = {id: 1, label: "Touch", color: "none"};
var event = {
id: paletteID,
type: "pointerPress",
position: ePosition,
user: eUser,
data: {button: "left"},
date: Date.now()
};
broadcast('eventInItem', event);
}
function sendDragToPalette(paletteID, x, y) {
var ePosition = {x: x, y: y};
var eUser = {id: 1, label: "Touch", color: "none"};
var event = {
id: paletteID,
type: "pointerDrag",
position: ePosition,
user: eUser,
data: {button: "left"},
date: Date.now()
};
broadcast('eventInItem', event);
}
function sendStyleToPalette(paletteID, style) {
var ePosition = {x: 0, y: 0};
var eUser = {id: 1, label: "Touch", color: "none"};
var event = {
id: paletteID,
type: "styleChange",
position: ePosition,
user: eUser,
data: {style: style},
date: Date.now()
};
broadcast('eventInItem', event);
}
function sendSessionListToPalette(paletteID, data) {
var ePosition = {x: 0, y: 0};
var eUser = {id: 1, label: "Touch", color: "none"};
var event = {
id: paletteID,
type: "sessionsList",
position: ePosition,
user: eUser,
data: data,
date: Date.now()
};
broadcast('eventInItem', event);
}
function sendChangeToPalette(paletteID, data) {
var ePosition = {x: 0, y: 0};
var eUser = {id: 1, label: "Touch", color: "none"};
var event = {
id: paletteID,
type: "modeChange",
position: ePosition,
user: eUser,
data: data,
date: Date.now()
};
broadcast('eventInItem', event);
}
function movePaletteTo(paletteID, x, y, w, h) {
var paletteApp = SAGE2Items.applications.list[paletteID];
if (paletteApp !== undefined) {
paletteApp.left = x;
paletteApp.top = y;
var moveApp = {
elemId: paletteID,
elemLeft: x,
elemTop: y,
elemWidth: w,
elemHeight: h,
date: new Date()
};
moveApplicationWindow(null, moveApp, null);
}
}
function setUpDialogsAsInteractableObjects() {
var dialogGeometry = {
x: config.totalWidth / 2 - 13 * config.ui.titleBarHeight,
y: 2 * config.ui.titleBarHeight,
w: 26 * config.ui.titleBarHeight,
h: 8 * config.ui.titleBarHeight
};
var acceptGeometry = {
x: dialogGeometry.x + 0.25 * config.ui.titleBarHeight,
y: dialogGeometry.y + 4.75 * config.ui.titleBarHeight,
w: 9 * config.ui.titleBarHeight,
h: 3 * config.ui.titleBarHeight
};
var rejectCancelGeometry = {
x: dialogGeometry.x + 16.75 * config.ui.titleBarHeight,
y: dialogGeometry.y + 4.75 * config.ui.titleBarHeight,
w: 9 * config.ui.titleBarHeight,
h: 3 * config.ui.titleBarHeight
};
interactMgr.addGeometry("dataSharingWaitDialog", "staticUI", "rectangle", dialogGeometry, false, 1, null);
interactMgr.addGeometry("dataSharingRequestDialog", "staticUI", "rectangle", dialogGeometry, false, 1, null);
interactMgr.addGeometry("acceptDataSharingRequest", "staticUI", "rectangle", acceptGeometry, false, 2, null);
interactMgr.addGeometry("cancelDataSharingRequest", "staticUI", "rectangle", rejectCancelGeometry, false, 2, null);
interactMgr.addGeometry("rejectDataSharingRequest", "staticUI", "rectangle", rejectCancelGeometry, false, 2, null);
}
/**
* Send a message to all clients using websocket
* @method broadcast
* @param name {String} name of the message
* @param data {Object} data of the message
*/
function broadcast(name, data) {
wsioServer.broadcast(name, data);
wsioServerS.broadcast(name, data);
}
/**
* Print a message to all the web consoles
*
* @method emitLog
* @param {Object} data object to print
*/
function emitLog(data) {
if (wsioServer === null || wsioServerS === null) {
return;
}
broadcast('console', data);
}
// Make the function global
global.emitLog = emitLog;
// Export variables to sub modules
// dirname: used by application plugins to load plugin source
// broadcast: used by plugins to send results back to apps
exports.dirname = path.join(__dirname, "node_modules");
exports.broadcast = broadcast;
// global variables to manage clients
var clients = [];
var masterDisplay = null;
var webBrowserClient = null;
var sagePointers = {};
var remoteInteraction = {};
var mediaBlockStreams = {};
var appUserColors = {}; // a dict to keep track of app instance colors(for widget connectors)
// var remoteSharingRequestDialog = null;
var remoteSharingWaitDialog = null;
var remoteSharingSessions = {};
// Sticky items and window position for new clones
var stickyAppHandler = new StickyItems();
// create manager for shared data
var sharedServerData = new SharedDataManager(clients, broadcast);
// create manager for voice actions, major functions are given on creation
// each of these needs to be memory references
var variablesUsedInVoiceHandler = {
config,
sagePointers,
interactMgr,
SAGE2Items,
assets,
listSessions,
sharedServerData,
wsCallFunctionOnApp,
wsLaunchAppWithValues,
wsLoadFileFromServer,
wsSaveSession,
tileApplications,
clearDisplay,
broadcast,
shareApplicationWithRemoteSite,
fillContextMenuWithShareSites,
remoteSites,
voiceNameMarker: config.voice_commands.system_name
};
var voiceHandler = new VoiceActionManager(variablesUsedInVoiceHandler);
//
// Catch the uncaught errors that weren't wrapped in a domain or try catch statement
//
process.on('uncaughtException', function(err) {
// handle the error safely
console.trace("SAGE2> ", err);
});
/**
* Callback when a client connects
*
* @method openWebSocketClient
* @param {Websocket} wsio The websocket for this client
*/
function openWebSocketClient(wsio) {
wsio.onclose(closeWebSocketClient);
wsio.on('addClient', wsAddClient);
}
/**
* Callback when a client closes
*
* @method closeWebSocketClient
* @param {Websocket} wsio websocket of the client
*/
function closeWebSocketClient(wsio) {
var i;
var key;
if (wsio.clientType === "display") {
sageutils.log("Disconnect", chalk.bold.red(wsio.id) +
" (" + wsio.clientType + " " + wsio.clientID + ")");
performanceManager.removeDisplayClient(wsio.id);
} else {
userlist.disconnect(wsio.id);
broadcast('userEvent', { type: 'disconnect', data: null, id: wsio.id });
if (wsio.clientType) {
sageutils.log("Disconnect", chalk.bold.red(wsio.id) + " (" + wsio.clientType + ")");
} else {
sageutils.log("Disconnect", chalk.bold.red(wsio.id) + " (unknown)");
}
}
addEventToUserLog(wsio.id, {type: "disconnect", data: null, time: Date.now()});
// if client is a remote site, send disconnect message
var remote = findRemoteSiteByConnection(wsio);
if (remote !== null) {
sageutils.log("Remote", chalk.cyan(remote.name), "now offline");
remote.connected = "off";
var site = {name: remote.name, connected: remote.connected};
broadcast('connectedToRemoteSite', site);
}
if (wsio.clientType === "sageUI") {
hidePointer(wsio.id);
removeControlsForUser(wsio.id);
delete sagePointers[wsio.id];
delete remoteInteraction[wsio.id];
for (key in remoteSharingSessions) {
remoteSharingSessions[key].wsio.emit('stopRemoteSagePointer', {id: wsio.id});
}
} else if (wsio.clientType === "display") {
for (key in SAGE2Items.renderSync) {
if (SAGE2Items.renderSync.hasOwnProperty(key)) {
// If the application had an animation timer, clear it
if (SAGE2Items.renderSync[key].clients[wsio.id] &&
SAGE2Items.renderSync[key].clients[wsio.id].animateTimer) {
clearTimeout(SAGE2Items.renderSync[key].clients[wsio.id].animateTimer);
}
// Remove the object from the list
delete SAGE2Items.renderSync[key].clients[wsio.id];
}
}
} else if (wsio.clientType === "webBrowser") {
webBrowserClient = null;
} else {
// if it's an application, assume it's a stream and try
deleteApplication(wsio.id + '|0');
}
if (wsio === masterDisplay) {
masterDisplay = null;
for (i = 0; i < clients.length; i++) {
if (clients[i].clientType === "display" && clients[i] !== wsio) {
masterDisplay = clients[i];
clients[i].emit('setAsMasterDisplay');
break;
}
}
}
removeElement(clients, wsio);
// Unregistering the client from the drawingManager
if (wsio.clientType === "display") {
drawingManager.removeWebSocket(wsio);
}
try {
// For debugging connections and slow down. Client just disconnected, update connection count.
sharedServerData.updateInformationAboutConnections(clients, sagePointers);
} catch (e) {
sageutils.log("Connections", "Error with updating client data");
sageutils.log("Connections", e);
}
}
/**
* Callback that configures a new client
*
* @method wsAddClient
* @param {Websocket} wsio client's websocket
* @param {Object} data initialization data
*/
function wsAddClient(wsio, data) {
// Check for password
if (config.passwordProtected) {
if (!data.session || data.session !== global.__SESSION_ID) {
sageutils.log("WebsocketIO", "wrong session hash - closing");
// Send a message back to server
wsio.emit('remoteConnection', {status: "refused", reason: 'wrong session hash'});
// If server protected and wrong hash, close the socket and byebye
wsio.ws.close();
// For debugging connections and slow down. This one logs failed connection attemps.
sharedServerData.updateInformationAboutConnectionsFailedRemoteSite(wsio);
return;
}
}
// Send a message back to server
wsio.emit('remoteConnection', {status: "accepted"});
// Just making sure the data is valid JSON (one gets strings from C++)
if (sageutils.isTrue(data.requests.config)) {
data.requests.config = true;
} else {
data.requests.config = false;
}
if (sageutils.isTrue(data.requests.version)) {
data.requests.version = true;
} else {
data.requests.version = false;
}
if (sageutils.isTrue(data.requests.time)) {
data.requests.time = true;
} else {
data.requests.time = false;
}
if (sageutils.isTrue(data.requests.console)) {
data.requests.console = true;
} else {
data.requests.console = false;
}
wsio.updateRemoteAddress(data.host, data.port); // overwrite host and port if defined
wsio.clientType = data.clientType;
if (wsio.clientType === "display") {
wsio.clientID = data.clientID;
if (masterDisplay === null) {
masterDisplay = wsio;
}
sageutils.log("Connect", chalk.bold.green(wsio.id) + " (" + wsio.clientType + " " + wsio.clientID + ")");
} else {
wsio.clientID = -1;
sageutils.log("Connect", chalk.bold.green(wsio.id) + " (" + wsio.clientType + ")");
if (wsio.clientType === "remoteServer") {
// Remote info
// var remoteaddr = wsio.ws.upgradeReq.connection.remoteAddress;
// var remoteport = wsio.ws.upgradeReq.connection.remotePort;
// Checking if it's a known server
// bug: Seems to create a race condition and works without, so far
// config.remote_sites.forEach(function(element, index, array) {
// if (element.host === data.host &&
// element.port === data.port &&
// remoteSites[index].connected === "on") {
// sageutils.log("Connect", 'known remote site', data.host, ':', data.port);
// manageRemoteConnection(wsio, element, index);
// }
// });
}
}
clients.push(wsio);
initializeWSClient(wsio, data.requests.config, data.requests.version, data.requests.time, data.requests.console);
if (wsio.clientType === "display") {
drawingManager.init(wsio);
}
// Check if there's a new pointer for a mobile client
if (data.browser && data.browser.isMobile && remoteInteraction[wsio.id]) {
// for mobile clients, default to window interaction mode
remoteInteraction[wsio.id].previousMode = 0;
}
// If it's a UI, send message to enable screenshot capability
if (wsio.clientType === "sageUI") {
reportIfCanWallScreenshot();
// Also tell it what name the system is called
wsio.emit("setVoiceNameMarker", {name: variablesUsedInVoiceHandler.voiceNameMarker});
}
// If it's a display, check for Electron and send enable screenshot capability
if (wsio.clientType === "display") {
// See in browser data structure if it's Electron
wsio.capableOfScreenshot = data.browser.isElectron;
// Send message to UI clients
reportIfCanWallScreenshot();
}
try {
// For debugging connections and slow down. Creates varibles apps can request for.
sharedServerData.updateInformationAboutConnections(clients, sagePointers);
} catch (e) {
sageutils.log("Connections", "Error with updating client data");
sageutils.log("Connections", e);
}
}
/**
* Sends the first messages when client built
*
* @method initializeWSClient
* @param {Websocket} wsio client's websocket
* @param {bool} reqConfig client requests configuration
* @param {bool} reqVersion client requests version
* @param {bool} reqTime client requests time information
* @param {bool} reqConsole client requests console messages
*/
function initializeWSClient(wsio, reqConfig, reqVersion, reqTime, reqConsole) {
setupListeners(wsio);
wsio.emit('initialize', {UID: wsio.id, time: Date.now(), start: startTime});
if (wsio === masterDisplay) {
wsio.emit('setAsMasterDisplay');
}
if (reqConfig) {
wsio.emit('setupDisplayConfiguration', config);
}
if (reqVersion) {
wsio.emit('setupSAGE2Version', SAGE2_version);
}
if (reqTime) {
var now = new Date();
wsio.emit('setSystemTime', {date: now.toJSON(), offset: now.getTimezoneOffset()});
}
if (reqConsole) {
wsio.emit('console', json5.stringify(config, null, 4));
}
if (wsio.clientType === "display") {
initializeExistingSagePointers(wsio);
initializeExistingPartitions(wsio);
initializeExistingApps(wsio);
initializeRemoteServerInfo(wsio);
initializeExistingWallUI(wsio);
setTimeout(initializeExistingControls, 6000, wsio); // why can't this be done immediately with the rest?
} else if (wsio.clientType === "audioManager") {
initializeExistingAppsAudio(wsio);
} else if (wsio.clientType === "sageUI") {
createSagePointer(wsio.id);
var key;
for (key in remoteSharingSessions) {
remoteSharingSessions[key].wsio.emit('createRemoteSagePointer', {
id: wsio.id, portal: {host: config.host, port: config.port}
});
}
initializeExistingAppsPositionSizeTypeOnly(wsio);
initializeExistingPartitionsUI(wsio);
}
var remote = findRemoteSiteByConnection(wsio);
if (remote !== null) {
remote.wsio = wsio;
remote.connected = "on";
var site = {name: remote.name, connected: remote.connected};
broadcast('connectedToRemoteSite', site);
}
if (wsio.clientType === "webBrowser") {
webBrowserClient = wsio;
}
if (wsio.clientType === "performance") {
performanceManager.updateClient(wsio);
}
}
/**
* Installs all the message callbacks on a websocket
*
* @method setupListeners
* @param {Websocket} wsio concerned websocket
*/
function setupListeners(wsio) {
wsio.on('registerInteractionClient', wsRegisterInteractionClient);
wsio.on('getActiveClients', wsGetActiveClients);
wsio.on('getRbac', wsGetRbac);
wsio.on('loginUser', wsLoginUser);
wsio.on('logoutUser', wsLogoutUser);
wsio.on('createUser', wsCreateUser);
wsio.on('editUser', wsEditUser);
wsio.on('editUserRole', wsEditUserRole);
wsio.on('editRole', wsEditRole);
wsio.on('createPermissionsModel', wsCreatePermissionsModel);
wsio.on('switchPermissionsModel', wsSwitchPermissionsModel);
wsio.on('startSagePointer', wsStartSagePointer);
wsio.on('stopSagePointer', wsStopSagePointer);
wsio.on('pointerPress', wsPointerPress);
wsio.on('pointerRelease', wsPointerRelease);
wsio.on('pointerDblClick', wsPointerDblClick);
wsio.on('pointerPosition', wsPointerPosition);
wsio.on('pointerMove', wsPointerMove);
wsio.on('pointerScrollStart', wsPointerScrollStart);
wsio.on('pointerScroll', wsPointerScroll);
wsio.on('pointerScrollEnd', wsPointerScrollEnd);
wsio.on('pointerDraw', wsPointerDraw);
wsio.on('keyDown', wsKeyDown);
wsio.on('keyUp', wsKeyUp);
wsio.on('keyPress', wsKeyPress);
wsio.on('uploadedFile', wsUploadedFile);
wsio.on('requestToStartMediaStream', wsRequestToStartMediaStream);
wsio.on('startNewMediaStream', wsStartNewMediaStream);
wsio.on('updateMediaStreamFrame', wsUpdateMediaStreamFrame);
wsio.on('updateMediaStreamChunk', wsUpdateMediaStreamChunk);
wsio.on('stopMediaStream', wsStopMediaStream);
wsio.on('startNewMediaBlockStream', wsStartNewMediaBlockStream);
wsio.on('updateMediaBlockStreamFrame', wsUpdateMediaBlockStreamFrame);
wsio.on('stopMediaBlockStream', wsStopMediaBlockStream);
wsio.on('requestVideoFrame', wsRequestVideoFrame);
wsio.on('receivedMediaStreamFrame', wsReceivedMediaStreamFrame);
wsio.on('receivedRemoteMediaStreamFrame', wsReceivedRemoteMediaStreamFrame);
wsio.on('receivedMediaBlockStreamFrame', wsReceivedMediaBlockStreamFrame);
wsio.on('receivedRemoteMediaBlockStreamFrame', wsReceivedRemoteMediaBlockStreamFrame);
wsio.on('finishedRenderingAppFrame', wsFinishedRenderingAppFrame);
wsio.on('updateAppState', wsUpdateAppState);
wsio.on('updateStateOptions', wsUpdateStateOptions);
wsio.on('appResize', wsAppResize);
wsio.on('appFullscreen', wsFullscreen);
wsio.on('broadcast', wsBroadcast);
wsio.on('applicationRPC', wsApplicationRPC);
wsio.on('requestAvailableApplications', wsRequestAvailableApplications);
wsio.on('requestStoredFiles', wsRequestStoredFiles);
wsio.on('loadApplication', wsLoadApplication);
wsio.on('loadFileFromServer', wsLoadFileFromServer);
wsio.on('loadImageFromBuffer', wsLoadImageFromBuffer);
wsio.on('deleteElementFromStoredFiles', wsDeleteElementFromStoredFiles);
wsio.on('moveElementFromStoredFiles', wsMoveElementFromStoredFiles);
wsio.on('saveSession', wsSaveSession);
wsio.on('clearDisplay', wsClearDisplay);
wsio.on('deleteAllApplications', wsDeleteAllApplications);
wsio.on('tileApplications', wsTileApplications);
// Radial menu should have its own message section? Just appended here for now.
wsio.on('radialMenuClick', wsRadialMenuClick);
wsio.on('radialMenuMoved', wsRadialMenuMoved);
wsio.on('removeRadialMenu', wsRemoveRadialMenu);
wsio.on('radialMenuWindowToggle', wsRadialMenuThumbnailWindow);
// DrawingState messages, should they have their own section?
wsio.on('updatePalettePosition', wsUpdatePalettePosition);
wsio.on('enableDrawingMode', wsEnableDrawingMode);
wsio.on('disableDrawingMode', wsDisableDrawingMode);
wsio.on('enableEraserMode', wsEnableEraserMode);
wsio.on('disableEraserMode', wsDisableEraserMode);
wsio.on('enablePointerColorMode', wsEnablePointerColorMode);
wsio.on('disablePointerColorMode', wsDisablePointerColorMode);
wsio.on('clearDrawingCanvas', wsClearDrawingCanvas);
wsio.on('changeStyle', wsChangeStyle);
wsio.on('undoLastDrawing', wsUndoLastDrawing);
wsio.on('redoDrawing', wsRedoDrawing);
wsio.on('loadDrawings', wsLoadDrawings);
wsio.on('getSessionsList', wsGetSessionsList);
wsio.on('saveDrawings', wsSaveDrawings);
wsio.on('enablePaintingMode', wsEnablePaintingMode);
wsio.on('disablePaintingMode', wsDisablePaintingMode);
wsio.on('saveScreenshot', wsSaveScreenshot);
wsio.on('selectionModeOnOff', wsSelectionModeOnOff);
wsio.on('addNewWebElement', wsAddNewWebElement);
wsio.on('openNewWebpage', wsOpenNewWebpage);
wsio.on('setVolume', wsSetVolume);
wsio.on('playVideo', wsPlayVideo);
wsio.on('pauseVideo', wsPauseVideo);
wsio.on('stopVideo', wsStopVideo);
wsio.on('updateVideoTime', wsUpdateVideoTime);
wsio.on('muteVideo', wsMuteVideo);
wsio.on('unmuteVideo', wsUnmuteVideo);
wsio.on('loopVideo', wsLoopVideo);
wsio.on('addNewElementFromRemoteServer', wsAddNewElementFromRemoteServer);
wsio.on('addNewSharedElementFromRemoteServer', wsAddNewSharedElementFromRemoteServer);
wsio.on('requestNextRemoteFrame', wsRequestNextRemoteFrame);
wsio.on('updateRemoteMediaStreamFrame', wsUpdateRemoteMediaStreamFrame);
wsio.on('stopMediaStream', wsStopMediaStream);
wsio.on('updateRemoteMediaBlockStreamFrame', wsUpdateRemoteMediaBlockStreamFrame);
wsio.on('stopMediaBlockStream', wsStopMediaBlockStream);
wsio.on('requestDataSharingSession', wsRequestDataSharingSession);
wsio.on('cancelDataSharingSession', wsCancelDataSharingSession);
wsio.on('acceptDataSharingSession', wsAcceptDataSharingSession);
wsio.on('rejectDataSharingSession', wsRejectDataSharingSession);
wsio.on('createRemoteSagePointer', wsCreateRemoteSagePointer);
wsio.on('startRemoteSagePointer', wsStartRemoteSagePointer);
wsio.on('stopRemoteSagePointer', wsStopRemoteSagePointer);
wsio.on('remoteSagePointerPosition', wsRemoteSagePointerPosition);
wsio.on('remoteSagePointerToggleModes', wsRemoteSagePointerToggleModes);
wsio.on('remoteSagePointerHoverCorner', wsRemoteSagePointerHoverCorner);
wsio.on('addNewRemoteElementInDataSharingPortal', wsAddNewRemoteElementInDataSharingPortal);
wsio.on('updateApplicationOrder', wsUpdateApplicationOrder);
wsio.on('startApplicationMove', wsStartApplicationMove);
wsio.on('startApplicationResize', wsStartApplicationResize);
wsio.on('updateApplicationPosition', wsUpdateApplicationPosition);
wsio.on('updateApplicationPositionAndSize', wsUpdateApplicationPositionAndSize);
wsio.on('finishApplicationMove', wsFinishApplicationMove);
wsio.on('finishApplicationResize', wsFinishApplicationResize);
wsio.on('deleteApplication', wsDeleteApplication);
wsio.on('updateApplicationState', wsUpdateApplicationState);
wsio.on('updateApplicationStateOptions', wsUpdateApplicationStateOptions);
wsio.on('addNewControl', wsAddNewControl);
wsio.on('closeAppFromControl', wsCloseAppFromControl);
wsio.on('hideWidgetFromControl', wsHideWidgetFromControl);
wsio.on('openRadialMenuFromControl', wsOpenRadialMenuFromControl);
wsio.on('recordInnerGeometryForWidget', wsRecordInnerGeometryForWidget);
wsio.on('requestNewTitle', wsRequestNewTitle);
wsio.on('requestFileBuffer', wsRequestFileBuffer);
wsio.on('closeFileBuffer', wsCloseFileBuffer);
wsio.on('updateFileBufferCursorPosition', wsUpdateFileBufferCursorPosition);
wsio.on('createAppClone', wsCreateAppClone);
wsio.on('sage2Log', wsPrintDebugInfo);
wsio.on('command', wsCommand);
wsio.on('createFolder', wsCreateFolder);
// Jupyper messages
wsio.on('startJupyterSharing', wsStartJupyterSharing);
wsio.on('updateJupyterSharing', wsUpdateJupyterSharing);
// message passing between clients
wsio.on('requestAppContextMenu', wsRequestAppContextMenu);
wsio.on('appContextMenuContents', wsAppContextMenuContents);
wsio.on('callFunctionOnApp', wsCallFunctionOnApp);
// generic message passing for data requests or for specific communications.
wsio.on('launchAppWithValues', wsLaunchAppWithValues);
wsio.on('sendDataToClient', wsSendDataToClient);
wsio.on('saveDataOnServer', wsSaveDataOnServer);
wsio.on('serverDataSetValue', wsServerDataSetValue);
wsio.on('serverDataGetValue', wsServerDataGetValue);
wsio.on('serverDataRemoveValue', wsServerDataRemoveValue);
wsio.on('serverDataSubscribeToValue', wsServerDataSubscribeToValue);
wsio.on('serverDataGetAllTrackedValues', wsServerDataGetAllTrackedValues);
wsio.on('serverDataGetAllTrackedDescriptions', wsServerDataGetAllTrackedDescriptions);
wsio.on('serverDataSubscribeToNewValueNotification',
wsServerDataSubscribeToNewValueNotification);
// voice to sage2 actions
wsio.on('voiceToAction', wsVoiceToAction);
// Screenshot messages
wsio.on('startWallScreenshot', wsStartWallScreenshot);
wsio.on('wallScreenshotFromDisplay', wsWallScreenshotFromDisplay);
// application file saving message
wsio.on('appFileSaveRequest', appFileSaveRequest);
// create partition
wsio.on('createPartition', wsCreatePartition);
wsio.on('partitionScreen', wsPartitionScreen);
wsio.on('deleteAllPartitions', wsDeleteAllPartitions);
wsio.on('partitionsGrabAllContent', wsPartitionsGrabAllContent);
// message from electron display client
wsio.on('displayHardware', wsDisplayHardware);
wsio.on('performanceData', wsPerformanceData);
// message from performance page
wsio.on('requestClientUpdate', wsRequestClientUpdate);
}
/**
* Ensures that new audioManager instances get metadata about all existing apps
*
* @method initializeExistingAppsAudio
* @param {Websocket} wsio client's websocket
*/
function initializeExistingAppsAudio(wsio) {
var key;
for (key in SAGE2Items.applications.list) {
wsio.emit('createAppWindow', SAGE2Items.applications.list[key]);
}
}
/**
* Rebuilds the application widgets for a given client
*
* @method initializeExistingControls
* @param {Websocket} wsio client's websocket
*/
function initializeExistingControls(wsio) {
var i;
var uniqueID;
var app;
// var zIndex;
var data;
var controlList = SAGE2Items.widgets.list;
for (i in controlList) {
if (controlList.hasOwnProperty(i) && SAGE2Items.applications.list.hasOwnProperty(controlList[i].appId)) {
data = controlList[i];
wsio.emit('createControl', data);
/*
zIndex = SAGE2Items.widgets.numItems;
var radialGeometry = {
x: data.left + (data.height / 2),
y: data.top + (data.height / 2),
r: data.height / 2
};
if (data.hasSideBar === true) {
var shapeData = {
radial: {
type: "circle",
visible: true,
geometry: radialGeometry
},
sidebar: {
type: "rectangle",
visible: true,
geometry: {
x: data.left + data.height,
y: data.top + (data.height / 2) - (data.barHeight / 2),
w: data.width - data.height, h: data.barHeight
}
}
};
interactMgr.addComplexGeometry(data.id, "widgets", shapeData, zIndex, data);
} else {
interactMgr.addGeometry(data.id, "widgets", "circle", radialGeometry, true, zIndex, data);
}
SAGE2Items.widgets.addItem(data);
*/
uniqueID = data.id.substring(data.appId.length, data.id.lastIndexOf("_"));
app = SAGE2Items.applications.list[data.appId];
addEventToUserLog(uniqueID, {type: "widgetMenu",
data: {action: "open", application: {id: app.id, type: app.application}},
time: Date.now()});
}
}
}
/**
* Rebuilds the pointers for a given client
*
* @method initializeExistingSagePointers
* @param {Websocket} wsio client's websocket
*/
function initializeExistingSagePointers(wsio) {
for (var key in sagePointers) {
if (sagePointers.hasOwnProperty(key)) {
wsio.emit('createSagePointer', sagePointers[key]);
wsio.emit('changeSagePointerMode', {id: sagePointers[key].id, mode: remoteInteraction[key].interactionMode});
}
}
}
/**
* Rebuilds the wall radial menu for a given client
*
* @method initializeExistingWallUI
* @param {Websocket} wsio client's websocket
*/
function initializeExistingWallUI(wsio) {
var menuInfo;
if (config.ui.reload_wallui_on_refresh === false) {
// console.log("WallUI reload on display client refresh: Disabled");
for (key in SAGE2Items.radialMenus.list) {
menuInfo = SAGE2Items.radialMenus.list[key].getInfo();
hideRadialMenu(menuInfo.id);
}
return;
}
// console.log("WallUI reload on display client refresh: Enabled (default)");
var key;
for (key in SAGE2Items.radialMenus.list) {
menuInfo = SAGE2Items.radialMenus.list[key].getInfo();
broadcast('createRadialMenu', menuInfo);
broadcast('updateRadialMenu', menuInfo);
updateWallUIMediaBrowser(menuInfo.id);
}
}
function initializeExistingApps(wsio) {
var key;
for (key in SAGE2Items.applications.list) {
// remove partition value from application while sending wsio message (circular structure)
// does this cause issues?
var appCopy = Object.assign({}, SAGE2Items.applications.list[key]);
delete appCopy.partition;
wsio.emit('createAppWindow', appCopy);
if (SAGE2Items.renderSync.hasOwnProperty(key)) {
SAGE2Items.renderSync[key].clients[wsio.id] = {wsio: wsio, readyForNextFrame: false, blocklist: []};
calculateValidBlocks(SAGE2Items.applications.list[key], mediaBlockSize, SAGE2Items.renderSync[key]);
// Need to reset the animation loop
// a new client could come while other clients were done rendering
// (especially true for slow update apps, like the clock)
broadcast('animateCanvas', {id: SAGE2Items.applications.list[key].id, date: Date.now()});
}
handleStickyItem(key);
}
for (key in SAGE2Items.portals.list) {
broadcast('initializeDataSharingSession', SAGE2Items.portals.list[key]);
}
var newOrder = interactMgr.getObjectZIndexList("applications", ["portals"]);
wsio.emit('updateItemOrder', newOrder);
}
function initializeExistingPartitions(wsio) {
var key;
for (key in partitions.list) {
wsio.emit('createPartitionWindow', partitions.list[key].getDisplayInfo());
wsio.emit('partitionWindowTitleUpdate', partitions.list[key].getTitle());
}
}
function initializeExistingAppsPositionSizeTypeOnly(wsio) {
var key;
for (key in SAGE2Items.applications.list) {
wsio.emit('createAppWindowPositionSizeOnly', getAppPositionSize(SAGE2Items.applications.list[key]));
// Send the appliation state to the UI
broadcast('applicationState', {
id: SAGE2Items.applications.list[key].id,
state: SAGE2Items.applications.list[key].data,
application: SAGE2Items.applications.list[key].application
});
handleStickyItem(key);
}
var newOrder = interactMgr.getObjectZIndexList("applications", ["portals"]);
wsio.emit('updateItemOrder', newOrder);
}
function initializeExistingPartitionsUI(wsio) {
var key;
for (key in partitions.list) {
wsio.emit('createPartitionBorder', partitions.list[key].getDisplayInfo());
}
}
function initializeRemoteServerInfo(wsio) {
for (var i = 0; i < remoteSites.length; i++) {
var site = {name: remoteSites[i].name, connected: remoteSites[i].connected, geometry: remoteSites[i].geometry};
wsio.emit('addRemoteSite', site);
}
}
// ************** Drawing Functions *****************
// The functions just call their associated method in the drawing manager
function wsUpdatePalettePosition(wsio, data) {
drawingManager.updatePalettePosition({
startX: data.x,
endX: data.x + data.w,
startY: data.y,
endY: data.y + data.h});
}
function wsEnableDrawingMode(wsio, data) {
drawingManager.enableDrawingMode(data);
}
function wsDisableDrawingMode(wsio, data) {
drawingManager.disableDrawingMode(data);
}
function wsEnableEraserMode(wsio, data) {
drawingManager.enableEraserMode(data);
}
function wsDisableEraserMode(wsio, data) {
drawingManager.disableEraserMode(data);
}
function wsEnablePointerColorMode(wsio, data) {
drawingManager.enablePointerColorMode(data);
}
function wsDisablePointerColorMode(wsio, data) {
drawingManager.disablePointerColorMode(data);
}
function wsClearDrawingCanvas(wsio, data) {
drawingManager.clearDrawingCanvas();
}
function wsChangeStyle(wsio, data) {
drawingManager.changeStyle(data);
}
function wsUndoLastDrawing(wsio, data) {
drawingManager.undoLastDrawing();
}
function wsRedoDrawing(wsio, data) {
drawingManager.redoDrawing();
}
function wsLoadDrawings(wsio, data) {
drawingManager.loadDrawings(data);
}
function wsGetSessionsList(wsio, data) {
var allDrawings = getAllDrawingsessions();
drawingManager.gotSessionsList(allDrawings);
}
function wsSaveDrawings(wsio, data) {
drawingManager.saveDrawings();
}
function wsEnablePaintingMode(wsio, data) {
drawingManager.enablePaintingMode();
}
function wsDisablePaintingMode(wsio, data) {
drawingManager.disablePaintingMode();
}
function wsSaveScreenshot(wsio, data) {
saveScreenshot(data.screenshot);
}
function wsSelectionModeOnOff(wsio, data) {
drawingManager.selectionModeOnOff();
}
// ************** User functions ****************
function wsGetActiveClients(wsio, data) {
broadcast('activeClientsRetrieved', {
clients: userlist.clients,
rbac: userlist.rbac
});
}
function wsGetRbac() {
broadcast('rbacRetrieved', {
rbac: userlist.rbac,
rbacList: userlist.rbacList
});
}
function wsLoginUser(wsio, data) {
let res = userlist.getUser(data.name, data.email);
// check if login was successful or not
if (res.error === null) {
userlist.track(wsio.id, res.user);
broadcast('userEvent', {
type: 'login',
data: res.user,
id: wsio.id
});
} else if (data.init) {
// first connection of an anonymous user
userlist.track(wsio.id, data);
broadcast('userEvent', {
type: 'connect',
data: data,
id: wsio.id
});
}
// return message to calling client
wsio.emit('loginStateChanged', {
login: true,
success: res.error === null,
uid: res.uid,
user: res.user || data,
errorMessage: res.error,
init: data.init
});
}
function wsLogoutUser(wsio, data) {
let res = userlist.getUserById(data);
if (res.error === null) {
// log out user and get anon name
let name = userlist.track(wsio.id, {
SAGE2_ptrColor: res.user.SAGE2_ptrColor
});
// log out all instances of the user on this server
for (let ip in userlist.clients) {
let user = userlist.clients[ip].user;
if (user && user.name === res.user.name && user.email === res.user.email) {
let foundWsio = clients.find(wsio => wsio.id === ip);
if (foundWsio) {
userlist.track(ip, {
SAGE2_ptrColor: res.user.SAGE2_ptrColor,
SAGE2_ptrName: name
});
foundWsio.emit('loginStateChanged', {
login: false,
name: name
});
}
}
}
wsio.emit('loginStateChanged', {
login: false,
name: name
});
broadcast('userEvent', {
type: 'logout',
data: res.user,
name: name,
id: wsio.id
});
}
}
function wsCreateUser(wsio, data) {
let res = userlist.addNewUser(data.name, data.email, {
SAGE2_ptrName: data.SAGE2_ptrName,
SAGE2_ptrColor: data.SAGE2_ptrColor
});
wsio.emit('loginStateChanged', {
login: true,
success: res.error === null,
uid: res.uid,
user: res.user,
errorMessage: res.error
});
if (res.error === null) {
userlist.track(wsio.id, res.user);
broadcast('userEvent', {
type: 'new user',
data: res.user,
id: wsio.id
});
}
}
function wsEditUser(wsio, data) {
let properties = data.properties;
if (data.uid) {
let success = userlist.editUser(data.uid, properties);
if (success) {
properties = userlist.getUserById(data.uid).user;
}
}
if (properties) {
if (properties.SAGE2_ptrColor && properties.SAGE2_ptrName) {
userlist.track(wsio.id, properties);
}
broadcast('userEvent', {
type: 'user edited',
data: properties,
id: wsio.id
});
}
}
function wsEditRole(wsio, data) {
if (data.hasRole) {
userlist.grantPermission(data.role, data.action);
} else {
userlist.revokePermission(data.role, data.action);
}
wsGetRbac();
}
function wsEditUserRole(wsio, data) {
if (data.ips) {
data.ips.forEach(ip => {
userlist.assignRole(ip, data.role);
});
wsGetActiveClients();
}
}
function wsCreatePermissionsModel(wsio, data) {
userlist.initRolesAndPermissions(Object.assign({
roles: userlist.rbac.roles.slice(),
actions: userlist.rbac.actions.slice(),
permissions: Object.assign({}, userlist.rbac.permissions)
}, data));
wsGetRbac();
}
function wsSwitchPermissionsModel(wsio, data) {
if (data == undefined) {
userlist.rbac = userlist.rbacList[0];
wsGetRbac();
} else {
let foundRbac = userlist.rbacList.find(rbac => rbac.name === data);
if (foundRbac) {
userlist.rbac = foundRbac;
wsGetRbac();
}
}
}
// ************** Sage Pointer Functions *****************
function wsRegisterInteractionClient(wsio, data) {
var key;
// Update color and name of pointer when UI connects
sagePointers[wsio.id].color = data.color;
sagePointers[wsio.id].name = data.name;
if (program.trackUsers === true) {
var newUser = true;
for (key in users) {
if (users[key].name === data.name && users[key].color.toLowerCase() === data.color.toLowerCase()) {
users[key].ip = wsio.id;
if (users[key].actions === undefined) {
users[key].actions = [];
}
users[key].actions.push({type: "connect", data: null, time: Date.now()});
newUser = false;
}
}
if (newUser === true) {
var id = getNewUserId();
users[id] = {};
users[id].name = data.name;
users[id].color = data.color;
users[id].ip = wsio.id;
if (users[id].actions === undefined) {
users[id].actions = [];
}
users[id].actions.push({type: "connect", data: null, time: Date.now()});
}
} else {
for (key in users) {
if (users[key].name === data.name && users[key].color.toLowerCase() === data.color.toLowerCase()) {
users[key].ip = wsio.id;
if (users[key].actions === undefined) {
users[key].actions = [];
}
users[key].actions.push({type: "connect", data: null, time: Date.now()});
}
}
}
}
function wsStartSagePointer(wsio, data) {
if (!userlist.isAllowed(wsio.id, 'share pointer')) {
wsio.emit('cancelAction', 'pointer');
return;
}
// Switch interaction from window mode (on web) to app mode (wall)
remoteInteraction[wsio.id].interactionMode = remoteInteraction[wsio.id].getPreviousMode();
broadcast('changeSagePointerMode', {id: sagePointers[wsio.id].id, mode: remoteInteraction[wsio.id].getPreviousMode()});
showPointer(wsio.id, data);
broadcast('userEvent', {type: 'start SAGE2 pointer', data: data, id: wsio.id});
addEventToUserLog(wsio.id, {type: "SAGE2PointerStart", data: null, time: Date.now()});
}
function wsStopSagePointer(wsio, data) {
hidePointer(wsio.id);
// return to window interaction mode after stopping pointer
remoteInteraction[wsio.id].saveMode();
if (remoteInteraction[wsio.id].appInteractionMode()) {
remoteInteraction[wsio.id].toggleModes();
broadcast('changeSagePointerMode', {id: sagePointers[wsio.id].id, mode: remoteInteraction[wsio.id].interactionMode});
}
var key;
for (key in remoteSharingSessions) {
remoteSharingSessions[key].wsio.emit('stopRemoteSagePointer', {id: wsio.id});
}
broadcast('userEvent', {type: 'stop SAGE2 pointer', data: data, id: wsio.id});
addEventToUserLog(wsio.id, {type: "SAGE2PointerEnd", data: null, time: Date.now()});
}
function wsPointerPress(wsio, data) {
if (userlist.isAllowed(wsio.id, 'move/resize windows')) {
var pointerX = sagePointers[wsio.id].left;
var pointerY = sagePointers[wsio.id].top;
pointerPress(wsio.id, pointerX, pointerY, data);
}
}
function wsPointerRelease(wsio, data) {
var pointerX = sagePointers[wsio.id].left;
var pointerY = sagePointers[wsio.id].top;
/*
if (data.button === 'left')
pointerRelease(wsio.id, pointerX, pointerY);
else
pointerReleaseRight(wsio.id, pointerX, pointerY);
*/
pointerRelease(wsio.id, pointerX, pointerY, data);
}
function wsPointerDblClick(wsio, data) {
if (userlist.isAllowed(wsio.id, 'move/resize windows')) {
var pointerX = sagePointers[wsio.id].left;
var pointerY = sagePointers[wsio.id].top;
pointerDblClick(wsio.id, pointerX, pointerY);
}
}
function wsPointerPosition(wsio, data) {
pointerPosition(wsio.id, data);
}
function wsPointerMove(wsio, data) {
var pointerX = sagePointers[wsio.id].left;
var pointerY = sagePointers[wsio.id].top;
pointerMove(wsio.id, pointerX, pointerY, data);
}
function wsPointerScrollStart(wsio, data) {
if (userlist.isAllowed(wsio.id, 'move/resize windows')) {
var pointerX = sagePointers[wsio.id].left;
var pointerY = sagePointers[wsio.id].top;
pointerScrollStart(wsio.id, pointerX, pointerY);
}
}
function wsPointerScroll(wsio, data) {
// Casting the parameters to correct type
data.wheelDelta = parseInt(data.wheelDelta, 10);
pointerScroll(wsio.id, data);
}
function wsPointerScrollEnd(wsio, data) {
pointerScrollEnd(wsio.id);
}
function wsPointerDraw(wsio, data) {
pointerDraw(wsio.id, data);
}
function wsKeyDown(wsio, data) {
var pointerX = sagePointers[wsio.id].left;
var pointerY = sagePointers[wsio.id].top;
keyDown(wsio.id, pointerX, pointerY, data);
}
function wsKeyUp(wsio, data) {
var pointerX = sagePointers[wsio.id].left;
var pointerY = sagePointers[wsio.id].top;
keyUp(wsio.id, pointerX, pointerY, data);
}
function wsKeyPress(wsio, data) {
var pointerX = sagePointers[wsio.id].left;
var pointerY = sagePointers[wsio.id].top;
keyPress(wsio.id, pointerX, pointerY, data);
}
// ************** File Upload Functions *****************
function wsUploadedFile(wsio, data) {
addEventToUserLog(wsio.id, {type: "fileUpload", data: data, time: Date.now()});
}
function wsRadialMenuClick(wsio, data) {
if (data.button === "closeButton") {
addEventToUserLog(data.user, {type: "radialMenu", data: {action: "close"}, time: Date.now()});
} else if (data.button === "settingsButton" || data.button.indexOf("Window") >= 0) {
var action = data.data.state === "opened" ? "open" : "close";
addEventToUserLog(data.user, {type: "radialMenuAction", data: {button: data.button, action: action}, time: Date.now()});
} else {
addEventToUserLog(data.user, {type: "radialMenuAction", data: {button: data.button}, time: Date.now()});
}
}
// ************** Media Stream Functions *****************
function wsRequestToStartMediaStream(wsio) {
if (!userlist.isAllowed(wsio.id, 'share screen')) {
wsio.emit('cancelAction', 'stream');
} else {
wsio.emit('allowAction', 'stream');
}
}
function wsStartNewMediaStream(wsio, data) {
sageutils.log("Media stream", 'new stream:', data.id);
var i;
SAGE2Items.renderSync[data.id] = {clients: {}, chunks: []};
for (i = 0; i < clients.length; i++) {
if (clients[i].clientType === "display") {
SAGE2Items.renderSync[data.id].clients[clients[i].id] = {wsio: clients[i], readyForNextFrame: false, blocklist: []};
}
}
// forcing 'int' type for width and height
data.width = parseInt(data.width, 10);
data.height = parseInt(data.height, 10);
appLoader.createMediaStream(data.src, data.type, data.encoding, data.title, data.color, data.width, data.height,
function(appInstance) {
appInstance.id = data.id;
handleNewApplication(appInstance, null);
var eLogData = {
application: {
id: appInstance.id,
type: appInstance.application
}
};
broadcast('userEvent', {type: 'media stream start', data: data, id: wsio.id });
addEventToUserLog(wsio.id, {type: "mediaStreamStart", data: eLogData, time: Date.now()});
});
}
/**
* Test if two rectangles overlap (axis-aligned)
*
* @method doOverlap
* @param x_1 {Integer} x coordinate first rectangle
* @param y_1 {Integer} y coordinate first rectangle
* @param width_1 {Integer} width first rectangle
* @param height_1 {Integer} height first rectangle
* @param x_2 {Integer} x coordinate second rectangle
* @param y_2 {Integer} y coordinate second rectangle
* @param width_2 {Integer} width second rectangle
* @param height_2 {Integer} height second rectangle
* @return {Boolean} true if rectangles overlap
*/
function doOverlap(x_1, y_1, width_1, height_1, x_2, y_2, width_2, height_2) {
return !(x_1 > x_2 + width_2 || x_1 + width_1 < x_2 || y_1 > y_2 + height_2 || y_1 + height_1 < y_2);
}
function wsUpdateMediaStreamFrame(wsio, data) {
var key;
// Remote sites have a pass back issue that needs to be caught
if (SAGE2Items.renderSync[data.id] === undefined || SAGE2Items.renderSync[data.id] === null) {
return;
}
// Reset the 'ready' flag for every display client
for (key in SAGE2Items.renderSync[data.id].clients) {
SAGE2Items.renderSync[data.id].clients[key].readyForNextFrame = false;
}
// Get the application from the message
var stream = SAGE2Items.applications.list[data.id];
if (stream !== undefined && stream !== null) {
stream.data = data.state;
} else {
// if can't find the application, it's being destroyed...
return;
}
// Send the image to all display nodes
// broadcast('updateMediaStreamFrame', data);
// Update the date
data.date = new Date();
// Create a copy of the frame object with dummy data (white 1x1 gif)
var data_copy = {};
data_copy.id = data.id;
data_copy.date = data.date;
data_copy.state = {};
data_copy.state.src = "R0lGODlhAQABAIABAP///wAAACwAAAAAAQABAAACAkQBADs=";
data_copy.state.type = "image/gif";
data_copy.state.encoding = "base64";
// Iterate over all the clients of this app
for (key in SAGE2Items.renderSync[data.id].clients) {
var did = SAGE2Items.renderSync[data.id].clients[key].wsio.clientID;
// Overview display
if (did === -1) {
// send the full frame to be displayed
SAGE2Items.renderSync[data.id].clients[key].wsio.emit('updateMediaStreamFrame', data);
continue;
}
var display = config.displays[did];
// app coordinates
var left = stream.left;
var top = stream.top + config.ui.titleBarHeight;
// tile coordinates
var offsetX = config.resolution.width * display.column;
var offsetY = config.resolution.height * display.row;
var checkWidth = config.resolution.width;
var checkHeight = config.resolution.height;
// Check for irregular tiles
checkWidth *= config.displays[did].width;
checkHeight *= config.displays[did].height;
// If the app window and the display overlap
if (doOverlap(left, top, stream.width, stream.height,
offsetX, offsetY, checkWidth, checkHeight)) {
// send the full frame to be displayed
SAGE2Items.renderSync[data.id].clients[key].wsio.emit('updateMediaStreamFrame', data);
} else {
// otherwise send a dummy small image
SAGE2Items.renderSync[data.id].clients[key].wsio.emit('updateMediaStreamFrame', data_copy);
}
}
}
function wsUpdateMediaStreamChunk(wsio, data) {
if (SAGE2Items.renderSync[data.id].chunks.length === 0) {
SAGE2Items.renderSync[data.id].chunks = initializeArray(data.total, "");
}
SAGE2Items.renderSync[data.id].chunks[data.piece] = data.state.src;
if (allNonBlank(SAGE2Items.renderSync[data.id].chunks)) {
wsUpdateMediaStreamFrame(wsio, {id: data.id, state: {
src: SAGE2Items.renderSync[data.id].chunks.join(""),
type: data.state.type,
encoding: data.state.encoding,
pointersOverApp: []}});
SAGE2Items.renderSync[data.id].chunks = [];
}
}
function wsStopMediaStream(wsio, data) {
var stream = SAGE2Items.applications.list[data.id];
broadcast('userEvent', {type: 'media stream stop', data: data, id: wsio.id});
if (stream !== undefined && stream !== null) {
deleteApplication(stream.id);
var eLogData = {
application: {
id: stream.id,
type: stream.application
}
};
addEventToUserLog(wsio.id, {type: "delete", data: eLogData, time: Date.now()});
}
// stop all clones in shared portals
var key;
for (key in SAGE2Items.portals.list) {
stream = SAGE2Items.applications.list[data.id + "_" + key];
if (stream !== undefined && stream !== null) {
deleteApplication(stream.id);
}
}
}
function wsReceivedMediaStreamFrame(wsio, data) {
SAGE2Items.renderSync[data.id].clients[wsio.id].readyForNextFrame = true;
if (allTrueDict(SAGE2Items.renderSync[data.id].clients, "readyForNextFrame")) {
var i;
var key;
for (key in SAGE2Items.renderSync[data.id].clients) {
SAGE2Items.renderSync[data.id].clients[key].readyForNextFrame = false;
}
var sender = {wsio: null, serverId: null, clientId: null, streamId: null};
var mediaStreamData = data.id.split("|");
if (mediaStreamData.length === 2) { // local stream --> client | stream_id
sender.clientId = mediaStreamData[0];
sender.streamId = parseInt(mediaStreamData[1]);
for (i = 0; i < clients.length; i++) {
if (clients[i].id === sender.clientId) {
sender.wsio = clients[i];
break;
}
}
if (sender.wsio !== null) {
sender.wsio.emit('requestNextFrame', {streamId: sender.streamId});
}
} else if (mediaStreamData.length === 3) { // remote stream --> remote_server | client | stream_id
sender.serverId = mediaStreamData[0];
sender.clientId = mediaStreamData[1];
sender.streamId = mediaStreamData[2];
for (i = 0; i < clients.length; i++) {
if (clients[i].id === sender.serverId) {
sender.wsio = clients[i];
break;
}
}
if (sender.wsio !== null) {
sender.wsio.emit('requestNextRemoteFrame', {id: sender.clientId + "|" + sender.streamId});
}
}
}
}
// ************** Media Block Stream Functions *****************
function wsStartNewMediaBlockStream(wsio, data) {
// Forcing 'int' type for width and height
// for some reasons, messages from websocket lib from Linux send strings for ints
data.width = parseInt(data.width, 10);
data.height = parseInt(data.height, 10);
sageutils.log("Block stream", data.width + 'x' + data.height, data.colorspace);
SAGE2Items.renderSync[data.id] = {chunks: [], clients: {}, width: data.width, height: data.height};
for (var i = 0; i < clients.length; i++) {
if (clients[i].clientType === "display") {
SAGE2Items.renderSync[data.id].clients[clients[i].id] = {wsio: clients[i], readyForNextFrame: true, blocklist: []};
}
}
appLoader.createMediaBlockStream(data.title, data.color, data.colorspace, data.width, data.height, function(appInstance) {
appInstance.id = data.id;
handleNewApplication(appInstance, null);
calculateValidBlocks(appInstance, mediaBlockSize, SAGE2Items.renderSync[appInstance.id]);
});
}
function wsUpdateMediaBlockStreamFrame(wsio, buffer) {
var i;
var key;
var id = byteBufferToString(buffer);
if (!SAGE2Items.applications.list.hasOwnProperty(id)) {
return;
}
for (key in SAGE2Items.renderSync[id].clients) {
SAGE2Items.renderSync[id].clients[key].readyForNextFrame = false;
}
var imgBuffer = buffer.slice(id.length + 1);
var colorspace = SAGE2Items.applications.list[id].data.colorspace;
var blockBuffers;
if (colorspace === "RGBA") {
blockBuffers = pixelblock.rgbaToPixelBlocks(imgBuffer, SAGE2Items.renderSync[id].width,
SAGE2Items.renderSync[id].height, mediaBlockSize);
} else if (colorspace === "RGB" || colorspace === "BGR") {
blockBuffers = pixelblock.rgbToPixelBlocks(imgBuffer, SAGE2Items.renderSync[id].width,
SAGE2Items.renderSync[id].height, mediaBlockSize);
} else if (colorspace === "YUV420p") {
blockBuffers = pixelblock.yuv420ToPixelBlocks(imgBuffer, SAGE2Items.renderSync[id].width,
SAGE2Items.renderSync[id].height, mediaBlockSize);
}
var pixelbuffer = [];
var idBuffer = Buffer.concat([new Buffer(id), new Buffer([0])]);
var dateBuffer = intToByteBuffer(Date.now(), 8);
var blockIdxBuffer;
for (i = 0; i < blockBuffers.length; i++) {
blockIdxBuffer = intToByteBuffer(i, 2);
pixelbuffer[i] = Buffer.concat([idBuffer, blockIdxBuffer, dateBuffer, blockBuffers[i]]);
}
for (key in SAGE2Items.renderSync[id].clients) {
for (i = 0; i < pixelbuffer.length; i++) {
if (SAGE2Items.renderSync[id].clients[key].blocklist.indexOf(i) >= 0) {
SAGE2Items.renderSync[id].clients[key].wsio.emit('updateMediaBlockStreamFrame', pixelbuffer[i]);
} else {
// this client has no blocks, so it is ready for next frame!
SAGE2Items.renderSync[id].clients[key].readyForNextFrame = true;
}
}
}
}
function wsStopMediaBlockStream(wsio, data) {
deleteApplication(data.id);
}
function wsReceivedMediaBlockStreamFrame(wsio, data) {
SAGE2Items.renderSync[data.id].clients[wsio.id].readyForNextFrame = true;
if (allTrueDict(SAGE2Items.renderSync[data.id].clients, "readyForNextFrame")) {
var i;
var key;
for (key in SAGE2Items.renderSync[data.id].clients) {
SAGE2Items.renderSync[data.id].clients[key].readyForNextFrame = false;
}
var sender = {wsio: null, serverId: null, clientId: null, streamId: null};
var mediaBlockStreamData = data.id.split("|");
if (mediaBlockStreamData.length === 2) { // local stream --> client | stream_id
sender.clientId = mediaBlockStreamData[0];
sender.streamId = parseInt(mediaBlockStreamData[1]);
for (i = 0; i < clients.length; i++) {
if (clients[i].id === sender.clientId) {
sender.wsio = clients[i];
break;
}
}
if (sender.wsio !== null) {
sender.wsio.emit('requestNextFrame', {streamId: sender.streamId});
}
} else if (mediaBlockStreamData.length === 3) { // remote stream --> remote_server | client | stream_id
sender.serverId = mediaBlockStreamData[0];
sender.clientId = mediaBlockStreamData[1];
sender.streamId = mediaBlockStreamData[2];
for (i = 0; i < clients.length; i++) {
if (clients[i].id === sender.serverId) {
sender.wsio = clients[i];
break;
}
}
if (sender.wsio !== null) {
sender.wsio.emit('requestNextRemoteFrame', {id: sender.clientId + "|" + sender.streamId});
}
}
}
}
// Print message from remote applications
function wsPrintDebugInfo(wsio, data) {
sageutils.log("Client", "Node " + data.node + " [" + data.app + "] " + data.message);
}
function wsRequestVideoFrame(wsio, data) {
SAGE2Items.renderSync[data.id].clients[wsio.id].readyForNextFrame = true;
handleNewClientReady(data.id);
}
// ************** Application Animation Functions *****************
function wsFinishedRenderingAppFrame(wsio, data) {
if (wsio === masterDisplay) {
SAGE2Items.renderSync[data.id].fps = data.fps;
}
SAGE2Items.renderSync[data.id].clients[wsio.id].readyForNextFrame = true;
if (allTrueDict(SAGE2Items.renderSync[data.id].clients, "readyForNextFrame")) {
var key;
for (key in SAGE2Items.renderSync[data.id].clients) {
SAGE2Items.renderSync[data.id].clients[key].readyForNextFrame = false;
}
var now = Date.now();
var elapsed = now - SAGE2Items.renderSync[data.id].date;
var fps = SAGE2Items.renderSync[data.id].fps || 30;
var ticks = 1000 / fps;
if (elapsed > ticks) {
SAGE2Items.renderSync[data.id].date = now;
broadcast('animateCanvas', {id: data.id, date: now});
} else {
var aTimer = setTimeout(function() {
now = Date.now();
SAGE2Items.renderSync[data.id].date = now;
broadcast('animateCanvas', {id: data.id, date: now});
}, ticks - elapsed);
SAGE2Items.renderSync[data.id].clients[wsio.id].animateTimer = aTimer;
}
}
}
function wsUpdateAppState(wsio, data) {
// Using updates only from master
if (wsio === masterDisplay && SAGE2Items.applications.list.hasOwnProperty(data.id)) {
var app = SAGE2Items.applications.list[data.id];
if (!app.data.pointersOverApp) {
// console.log("erase me, something removed the pointersOverApp property. Readding...");
// app.data.pointersOverApp = [];
}
sageutils.mergeObjects(data.localState, app.data, ['doc_url', 'video_url', 'video_type', 'audio_url', 'audio_type']);
if (data.updateRemote === true) {
var ts;
var portal = findApplicationPortal(app);
if (portal !== undefined && portal !== null) {
ts = Date.now() + remoteSharingSessions[portal.id].timeOffset;
remoteSharingSessions[portal.id].wsio.emit('updateApplicationState', {
id: data.id, state: data.remoteState, date: ts
});
} else if (sharedApps[data.id] !== undefined) {
var i;
for (i = 0; i < sharedApps[data.id].length; i++) {
// var ts = Date.now() + remoteSharingSessions[portal.id].timeOffset;
ts = Date.now();
sharedApps[data.id][i].wsio.emit('updateApplicationState',
{id: sharedApps[data.id][i].sharedId, state: data.remoteState, date: ts});
}
}
}
// Send the appliation state to the UI
broadcast('applicationState', {
id: data.id,
state: app.data,
application: app.application
});
}
}
function wsUpdateStateOptions(wsio, data) {
if (wsio === masterDisplay && SAGE2Items.applications.list.hasOwnProperty(data.id)) {
if (sharedApps[data.id] !== undefined) {
var i;
for (i = 0; i < sharedApps[data.id].length; i++) {
// var ts = Date.now() + remoteSharingSessions[portal.id].timeOffset;
var ts = Date.now();
sharedApps[data.id][i].wsio.emit('updateApplicationStateOptions',
{id: sharedApps[data.id][i].sharedId, options: data.options, date: ts});
}
}
}
}
//
// Got a resize call for an application itself
//
function wsAppResize(wsio, data) {
if (SAGE2Items.applications.list.hasOwnProperty(data.id)) {
var app = SAGE2Items.applications.list[data.id];
// Values in percent if smaller than 1
if (data.width > 0 && data.width <= 1) {
data.width = Math.round(data.width * config.totalWidth);
}
if (data.height > 0 && data.height <= 1) {
data.height = Math.round(data.height * config.totalHeight);
}
// Update the width height and aspect ratio
if (sageutils.isTrue(data.keepRatio)) {
// we use the width as leading the calculation
app.width = data.width;
app.height = data.width / app.aspect;
} else {
app.width = data.width;
app.height = data.height;
app.aspect = app.width / app.height;
app.native_width = data.width;
app.native_height = data.height;
}
// build the object to be sent
var updateItem = {
elemId: app.id,
elemLeft: app.left,
elemTop: app.top,
elemWidth: app.width,
elemHeight: app.height,
force: true,
date: Date.now()
};
moveAndResizeApplicationWindow(updateItem);
}
}
//
// Move the application relative to its position
//
function wsAppMoveBy(wsio, data) {
if (SAGE2Items.applications.list.hasOwnProperty(data.id)) {
var app = SAGE2Items.applications.list[data.id];
// Values in percent if smaller than 1
if (data.dx > 0 && data.dx < 1) {
data.dx = Math.round(data.dx * config.totalWidth);
}
if (data.dy > 0 && data.dy < 1) {
data.dy = Math.round(data.dy * config.totalHeight);
}
app.left += data.dx;
app.top += data.dy;
// build the object to be sent
var updateItem = {
elemId: app.id,
elemLeft: app.left,
elemTop: app.top,
elemWidth: app.width,
elemHeight: app.height,
force: true,
date: Date.now()
};
moveAndResizeApplicationWindow(updateItem);
}
}
//
// Move the application relative to its position
//
function wsAppMoveTo(wsio, data) {
if (SAGE2Items.applications.list.hasOwnProperty(data.id)) {
var app = SAGE2Items.applications.list[data.id];
// Values in percent if smaller than 1
if (data.x > 0 && data.x <= 1) {
data.x = Math.round(data.x * config.totalWidth);
}
if (data.y > 0 && data.y <= 1) {
data.y = Math.round(data.y * config.totalHeight);
}
app.left = data.x;
app.top = data.y;
// build the object to be sent
var updateItem = {
elemId: app.id,
elemLeft: app.left,
elemTop: app.top,
elemWidth: app.width,
elemHeight: app.height,
force: true,
date: Date.now()
};
moveAndResizeApplicationWindow(updateItem);
}
}
//
// Application request fullscreen
//
function wsFullscreen(wsio, data) {
var id = data.id;
if (SAGE2Items.applications.list.hasOwnProperty(id)) {
var item = SAGE2Items.applications.list[id];
var wallRatio = config.totalWidth / config.totalHeight;
var iCenterX = config.totalWidth / 2.0;
var iCenterY = config.totalHeight / 2.0;
var iWidth = 1;
var iHeight = 1;
var titleBar = config.ui.titleBarHeight;
if (config.ui.auto_hide_ui === true) {
titleBar = 0;
}
if (item.aspect > wallRatio) {
// Image wider than wall
iWidth = config.totalWidth;
iHeight = iWidth / item.aspect;
} else {
// Wall wider than image
iHeight = config.totalHeight - (2 * titleBar);
iWidth = iHeight * item.aspect;
}
// back up values for restore
item.previous_left = item.left;
item.previous_top = item.top;
item.previous_width = item.width;
item.previous_height = item.width / item.aspect;
// calculate new values
item.left = iCenterX - (iWidth / 2);
item.top = iCenterY - (iHeight / 2);
item.width = iWidth;
item.height = iHeight;
// Shift by 'titleBarHeight' if no auto-hide
if (config.ui.auto_hide_ui === true) {
item.top = item.top - config.ui.titleBarHeight;
}
item.maximized = true;
// build the object to be sent
var updateItem = {elemId: item.id, elemLeft: item.left, elemTop: item.top,
elemWidth: item.width, elemHeight: item.height, force: true,
date: new Date()};
moveAndResizeApplicationWindow(updateItem);
}
}
//
// Broadcast data to all clients who need apps
//
function wsBroadcast(wsio, data) {
broadcast('broadcast', data);
}
//
// RPC call from apps
//
function wsApplicationRPC(wsio, data) {
var app = SAGE2Items.applications.list[data.app];
if (app && app.plugin) {
// Find the path to the app plugin
var pluginFile = path.resolve(app.file, app.plugin);
try {
// Loading the plugin using builtin require function
var rpcFunction = require(pluginFile);
// Start the function inside the plugin
rpcFunction(wsio, data, config);
} catch (e) {
// If something fails
console.log("----------------------------");
sageutils.log('RPC', 'error in plugin', pluginFile);
console.log(e);
console.log("----------------------------");
}
} else {
sageutils.log('RPC', 'error no plugin found for', app.file);
}
}
// ************** Session Functions *****************
function wsSaveSession(wsio, data) {
var sname = "";
if (data) {
// If a name is passed, use it
sname = data;
} else {
// Otherwise use the date in the name
var ad = new Date();
sname = sprint("session_%4d_%02d_%02d_%02d_%02d_%02s",
ad.getFullYear(), ad.getMonth() + 1, ad.getDate(),
ad.getHours(), ad.getMinutes(), ad.getSeconds()
);
}
saveSession(sname);
}
function printListSessions() {
var thelist = listSessions();
console.log("Sessions\n---------");
for (var i = 0; i < thelist.length; i++) {
console.log(sprint("%2d: Name: %s\tSize: %.0fKB\tDate: %s",
i, thelist[i].exif.FileName, thelist[i].exif.FileSize / 1024.0, thelist[i].exif.FileDate
));
}
}
function listSessions() {
var thelist = [];
// Walk through the session files: sync I/Os to build the array
var files = fs.readdirSync(sessionDirectory);
for (var i = 0; i < files.length; i++) {
var file = files[i];
var filename = path.join(sessionDirectory, file);
var stat = fs.statSync(filename);
// is it a file
if (stat.isFile()) {
// doest it ends in .json
if (filename.indexOf(".json", filename.length - 5) >= 0) {
// use its change time (creation, update, ...)
var ad = new Date(stat.mtime);
var strdate = sprint("%4d/%02d/%02d %02d:%02d:%02s",
ad.getFullYear(), ad.getMonth() + 1, ad.getDate(),
ad.getHours(), ad.getMinutes(), ad.getSeconds()
);
// create path to thumbnail
var thumbPath = path.join(path.join(path.join("", "user"), "sessions"), ".previews");
// replace .json with .svg in filename
var thumbPathFull = "\\" + path.join(thumbPath, file.substring(".json", file.length - 5) + ".svg");
// Make it look like an exif data structure
thelist.push({id: filename,
sage2URL: '/uploads/' + file,
exif: { FileName: file.slice(0, -5),
FileSize: stat.size,
FileDate: strdate,
MIMEType: 'sage2/session',
SAGE2thumbnail: thumbPathFull
}
});
}
}
}
return thelist;
}
function deleteSession(filename, cb) {
if (filename) {
// var fullpath = path.join(sessionDirectory, filename);
// // if it doesn't end in .json, add it
// if (fullpath.indexOf(".json", fullpath.length - 5) === -1) {
// fullpath += '.json';
// }
var fullpath = path.resolve(filename);
fs.unlink(fullpath, function(err) {
if (err) {
sageutils.log("Session", "Could not delete session", filename, err);
return;
}
sageutils.log("Session", "Successfully deleted session", filename);
if (cb) {
cb();
}
});
}
}
function saveDrawingSession(data) {
var now = new Date();
var filename = "drawingSession" + now.getTime();
var fullpath = path.join(sessionDirectory, filename);
// if it doesn't end in .json, add it
if (fullpath.indexOf(".json", fullpath.length - 5) === -1) {
fullpath += '.json';
}
try {
fs.writeFileSync(fullpath, JSON.stringify(data, null, 4));
sageutils.log("Session", "saved drawing session file to", fullpath);
} catch (err) {
sageutils.log("Session", "error saving", err);
}
}
function getAllDrawingsessions() {
var allNames = fs.readdirSync(sessionDirectory);
var res = [];
for (var i in allNames) {
if (allNames[i].indexOf("drawingSession") != -1) {
res.push(allNames[i]);
}
}
return res;
}
function loadDrawingSession(filename) {
if (filename == null) {
console.log("Filename does not exist");
filename = "drawingSession";
}
var fullpath;
if (sageutils.fileExists(path.resolve(filename))) {
fullpath = filename;
} else {
fullpath = path.join(sessionDirectory, filename);
}
// if it doesn't end in .json, add it
if (fullpath.indexOf(".json", fullpath.length - 5) === -1) {
fullpath += '.json';
}
fs.readFile(fullpath, function(err, data) {
if (err) {
console.log("Error reading DrawingState: ", err);
} else {
console.log("Reading DrawingState from " + fullpath);
var j = JSON.parse(data);
drawingManager.loadOldState(j);
}
});
}
function saveScreenshot(data) {
var now = new Date();
// Assign a unique name
var filename = "screenshot" + now.getTime() + '.png';
var img = data.replace("data:image/png;base64,", "");
var fullpath = path.join(whiteboardDirectory, filename);
var buf = new Buffer(img, 'base64');
try {
fs.writeFile(fullpath, buf);
sageutils.log("Session", "saved screenshot file to", fullpath);
} catch (err) {
sageutils.log("Session", "error saving", err);
}
}
function saveSession(filename) {
filename = filename || 'default.json';
var key;
var states = {};
states.apps = [];
states.numapps = 0;
states.partitions = [];
states.numpartitions = 0;
states.date = Date.now();
for (key in SAGE2Items.applications.list) {
// make a copy of the application object
var a = Object.assign({}, SAGE2Items.applications.list[key]);
if (a.partition) {
// remove reference to parent partition if it exists
delete a.partition;
}
// Test if the application is shared (coming from another server)
// appId contains a + character
var isNotShared = (a.id.indexOf('+') === -1);
// Delete circular structures about the sticky applications
if (a.foregroundItems) {
delete a.foregroundItems;
}
if (a.backgroundItem) {
a.sticky = false;
delete a.backgroundItem;
}
// Ignore media streaming applications for now (desktop sharing) and shared applications
if (a.application !== 'media_stream' &&
a.application !== 'media_block_stream' &&
isNotShared) {
states.apps.push(a);
states.numapps++;
}
}
// Delete circular reference in the state of partitions
for (key in partitions.list) {
var p = Object.assign({}, partitions.list[key]);
if (p.partitionList) {
delete p.partitionList;
}
for (var app in p.children) {
p.children[app] = Object.assign({}, p.children[app]);
if (p.children[app].partition) {
delete p.children[app].partition;
}
}
states.partitions.push(p);
states.numpartitions++;
}
// session with only partitions considered a "LAYOUT"
if (states.numapps === 0 && states.numpartitions > 0 && filename !== "default.json") {
filename = "LAYOUT - " + filename;
}
var fullpath = path.join(sessionDirectory, filename);
// if it doesn't end in .json, add it
if (fullpath.indexOf(".json", fullpath.length - 5) === -1) {
fullpath += '.json';
}
// save session preview image to sessions/.previews/
var previewPath = path.join(sessionDirectory, ".previews");
if (!sageutils.folderExists(previewPath)) {
sageutils.mkdirParent(previewPath);
}
var previewFname;
if (filename.indexOf(".json", filename.length - 5) === -1) {
previewFname = filename + ".svg";
} else {
previewFname = filename.substr(0, filename.length - 5) + ".svg";
}
var fullPreviewPath = path.join(previewPath, previewFname);
// create svg string as thumbnail for session preview
var width = config.totalWidth,
height = config.totalHeight,
box = "0,0," + width + "," + height;
var svg = "<svg width=\"" + 256 +
"\" height=\"" + 256 +
"\" viewBox=\"" + box +
"\" version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\" " +
"xmlns:xlink=\"http://www.w3.org/1999/xlink\">";
// add gray background
svg += "<rect width=\"" + width +
"\" height=\"" + height +
"\" style=\"fill: #666666;\"" + "></rect>";
for (var ptn of states.partitions) {
// partition areas
svg += "<rect width=\"" + (ptn.width - 8) +
"\" height=\"" + (ptn.height - 8) +
"\" x=\"" + (ptn.left + 4) +
"\" y=\"" + (ptn.top + 4) +
"\" style=\"fill: " + ptn.color +
"; stroke: " + ptn.color +
"; stroke-width: 8; fill-opacity: 0.3;\"" + "></rect>";
// partition title bars
svg += "<rect width=\"" + ptn.width +
"\" height=\"" + config.ui.titleBarHeight +
"\" x=\"" + ptn.left +
"\" y=\"" + (ptn.top - config.ui.titleBarHeight) +
"\" style=\"fill: " + ptn.color +
"\"" + "></rect>";
}
for (var ap of states.apps) {
// draw app rectangles
svg += "<rect width=\"" + ap.width +
"\" height=\"" + ap.height +
"\" x=\"" + ap.left +
"\" y=\"" + ap.top +
"\" style=\"fill: " + "#AAAAAA; fill-opacity: 0.5; stroke: black; stroke-width: 5;\">" + "</rect>";
var iconPath;
if (ap.icon) {
// the application has a icon defined
iconPath = path.join(mainFolder.path, path.relative("/user", ap.icon)) + "_256.jpg";
} else {
// application does not have an icon (for instance, shared applciation)
iconPath = path.join(mainFolder.path, "assets/apps/unknownapp") + "_256.jpg";
}
var iconImageData = "";
try {
iconImageData = new Buffer(fs.readFileSync(iconPath)).toString('base64');
} catch (error) {
// error reading/converting icon image
}
svg += "<image width=\"" + ap.width +
"\" height=\"" + ap.height +
"\" x=\"" + ap.left +
"\" y=\"" + ap.top +
"\" xlink:href=\"data:image/jpg;base64," + iconImageData + "\">" + "</image>";
}
svg += "</svg>";
// svg file header
var header = "<?xml version=\"1.0\" encoding=\"utf-8\"?>";
header += "<!DOCTYPE svg PUBLIC \"-//W3C//DTD SVG 1.1//EN\" \"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd\">";
try {
fs.writeFileSync(fullpath, JSON.stringify(states, null, 4));
sageutils.log("Session", "saved session file to", chalk.yellow.bold(fullpath));
} catch (err) {
sageutils.log("Session", "error saving", err);
}
// write preview image
try {
fs.writeFileSync(fullPreviewPath, header + svg);
sageutils.log("Session", "saved session preview image to", chalk.yellow.bold(fullPreviewPath));
// Update the file manager in the UI clients
broadcast('storedFileList', getSavedFilesList());
} catch (err) {
sageutils.log("Session", "error saving", err);
}
}
function saveUserLog(filename) {
if (users !== null) {
filename = filename || "user-log_" + formatDateToYYYYMMDD_HHMMSS(new Date(startTime)) + ".json";
users.session.end = Date.now();
var userLogName = path.join("logs", filename);
if (sageutils.fileExists(userLogName)) {
fs.unlinkSync(userLogName);
}
var ignoreIP = function(key, value) {
if (key === "ip") {
return undefined;
}
return value;
};
fs.writeFileSync(userLogName, json5.stringify(users, ignoreIP, 4));
sageutils.log("LOG", "saved log file to", userLogName);
}
}
function createAppFromDescription(app, callback) {
sageutils.log("Session", "App", app.id);
if (app.application === "media_stream" || app.application === "media_block_stream") {
callback(JSON.parse(JSON.stringify(app)), null);
return;
}
var cloneApp = function(appInstance, videohandle) {
appInstance.left = app.left;
appInstance.top = app.top;
appInstance.width = app.width;
appInstance.height = app.height;
appInstance.previous_left = app.previous_left;
appInstance.previous_top = app.previous_top;
appInstance.previous_width = app.previous_width;
appInstance.previous_height = app.previous_height;
appInstance.maximized = app.maximized;
sageutils.mergeObjects(app.data, appInstance.data, ['doc_url', 'video_url', 'video_type', 'audio_url', 'audio_type']);
callback(appInstance, videohandle);
};
var appURL = url.parse(app.url);
if (appURL.hostname === config.host) {
if (app.application === "image_viewer" || app.application === "pdf_viewer" || app.application === "movie_player") {
appLoader.loadFileFromLocalStorage({application: app.application, filename: appURL.path}, cloneApp);
} else {
appLoader.loadFileFromLocalStorage({application: "custom_app", filename: appURL.path}, cloneApp);
}
} else {
if (app.application === "image_viewer" || app.application === "pdf_viewer" || app.application === "movie_player") {
appLoader.loadFileFromWebURL({url: app.url, type: app.type}, cloneApp);
} else {
appLoader.loadApplicationFromRemoteServer(app, cloneApp);
}
}
return app.id;
}
function loadSession(filename) {
filename = filename || 'default.json';
var fullpath;
if (sageutils.fileExists(path.resolve(filename))) {
fullpath = filename;
} else {
fullpath = path.join(sessionDirectory, filename);
}
// if it doesn't end in .json, add it
if (fullpath.indexOf(".json", fullpath.length - 5) === -1) {
fullpath += '.json';
}
fs.readFile(fullpath, function(err, data) {
if (err) {
sageutils.log("SAGE2", "error reading session", err);
} else {
sageutils.log("SAGE2", "reading session from " + fullpath);
var session = JSON.parse(data);
sageutils.log("Session", "number of applications", session.numapps);
// recreate partitions
if (session.partitions) {
// if there are any existing partitions
if (partitions.count > 0) {
// remove them and replace with partitions from sessions
for (var id of Object.keys(partitions.list)) {
deletePartition(id);
}
}
session.partitions.forEach(function(element, index, array) {
// remake partition
var ptn = createPartition(
{
width: element.width,
height: element.height,
left: element.left,
top: element.top,
isSnapping: element.isSnapping
},
element.color
);
ptn.innerMaximization = element.innerMaximization;
ptn.innerTiling = element.innerTiling;
broadcast('partitionWindowTitleUpdate', ptn.getTitle());
});
}
// Assign the windows to partitions
// don't assign existing content to partitions from session
// partitionsGrabAllContent();
// recreate apps
session.apps.forEach(function(element, index, array) {
createAppFromDescription(element, function(appInstance, videohandle) {
appInstance.id = getUniqueAppId();
if (appInstance.animation) {
var i;
SAGE2Items.renderSync[appInstance.id] = {clients: {}, date: Date.now()};
for (i = 0; i < clients.length; i++) {
if (clients[i].clientType === "display") {
SAGE2Items.renderSync[appInstance.id].clients[clients[i].id] = {wsio: clients[i],
readyForNextFrame: false, blocklist: []};
}
}
}
handleNewApplication(appInstance, videohandle);
});
});
}
});
}
// ************** Information Functions *****************
function listClients() {
var i;
console.log("Clients (%d)\n------------", clients.length);
for (i = 0; i < clients.length; i++) {
if (clients[i].clientType === "display") {
if (clients[i] === masterDisplay) {
console.log(sprint("%2d: %s (%s %s) master", i, clients[i].id, clients[i].clientType, clients[i].clientID));
} else {
console.log(sprint("%2d: %s (%s %s)", i, clients[i].id, clients[i].clientType, clients[i].clientID));
}
} else {
console.log(sprint("%2d: %s (%s)", i, clients[i].id, clients[i].clientType));
}
}
}
function listMediaStreams() {
var i, c, key;
console.log("Block streams (%d)\n------------", Object.keys(mediaBlockStreams).length);
i = 0;
for (key in mediaBlockStreams) {
var numclients = Object.keys(mediaBlockStreams[key].clients).length;
console.log(sprint("%2d: %s ready:%s clients:%d", i, key, mediaBlockStreams[key].ready, numclients));
var cstr = " ";
for (c in mediaBlockStreams[key].clients) {
cstr += c + "(" + mediaBlockStreams[key].clients[c] + ") ";
}
console.log("\t", cstr);
i++;
}
console.log("Media streams\n------------");
for (key in SAGE2Items.applications.list) {
var app = SAGE2Items.applications.list[key];
if (app.application === "media_stream") {
console.log(sprint("%2d: %s %s %s",
i, app.id, app.application, app.title));
i++;
}
}
}
function listMediaBlockStreams() {
listMediaStreams();
}
function listApplications() {
var i = 0;
var key;
console.log("Applications\n------------");
for (key in SAGE2Items.applications.list) {
var app = SAGE2Items.applications.list[key];
console.log(sprint("%2d: %s %s [%dx%d +%d+%d] %s (v%s) by %s",
i, app.id, app.application,
app.width, app.height,
app.left, app.top,
app.title, app.metadata.version,
app.metadata.author));
i++;
}
}
// ************** Tiling Functions *****************
//
//
// From Ratko's DIM in SAGE
// adapted to use all the tiles
// and center of gravity
function averageWindowAspectRatio() {
var num = SAGE2Items.applications.numItems;
if (num === 0) {
return 1.0;
}
var totAr = 0.0;
var key;
for (key in SAGE2Items.applications.list) {
totAr += (SAGE2Items.applications.list[key].width / SAGE2Items.applications.list[key].height);
}
return (totAr / num);
}
function fitWithin(app, x, y, width, height, margin) {
var titleBar = config.ui.titleBarHeight;
if (config.ui.auto_hide_ui === true) {
titleBar = 0;
}
// take buffer into account
x += margin;
y += margin;
width = width - 2 * margin;
height = height - 2 * margin;
var widthRatio = (width - titleBar) / app.width;
var heightRatio = (height - titleBar) / app.height;
var maximizeRatio;
if (widthRatio > heightRatio) {
maximizeRatio = heightRatio;
} else {
maximizeRatio = widthRatio;
}
// figure out the maximized app size (w/o the widgets)
var newAppWidth = Math.round(maximizeRatio * app.width);
var newAppHeight = Math.round(maximizeRatio * app.height);
// figure out the maximized app position (with the widgets)
var postMaxX = Math.round(width / 2.0 - newAppWidth / 2.0);
var postMaxY = Math.round(height / 2.0 - newAppHeight / 2.0);
// the new position of the app considering the maximized state and
// all the widgets around it
var newAppX = x + postMaxX;
var newAppY = y + postMaxY;
return [newAppX, newAppY, newAppWidth, newAppHeight];
}
// Calculate the square of euclidian distance between two objects with .x and .y fields
function distanceSquared2D(p1, p2) {
var dx = p2.x - p1.x;
var dy = p2.y - p1.y;
return (dx * dx + dy * dy);
}
function findMinimum(arr) {
var val = Number.MAX_VALUE;
var idx = 0;
for (var i = 0; i < arr.length; i++) {
if (arr[i] < val) {
val = arr[i];
idx = i;
}
}
return idx;
}
function tileApplications() {
var app;
var i, c, r, key;
var numCols, numRows, numCells;
var displayAr = config.totalWidth / config.totalHeight;
var arDiff = displayAr / averageWindowAspectRatio();
var backgroundAndForegroundItems = stickyAppHandler.getListOfBackgroundAndForegroundItems(SAGE2Items.applications.list);
var appsWithoutBackground = backgroundAndForegroundItems.backgroundItems;
var numAppsWithoutBackground = appsWithoutBackground.length;
// Don't use sticking items to compute number of windows.
var numWindows = numAppsWithoutBackground;
//var numWindows = SAGE2Items.applications.numItems;
// 3 scenarios... windows are on average the same aspect ratio as the display
if (arDiff >= 0.7 && arDiff <= 1.3) {
numCols = Math.ceil(Math.sqrt(numWindows));
numRows = Math.ceil(numWindows / numCols);
} else if (arDiff < 0.7) {
// windows are much wider than display
c = Math.round(1 / (arDiff / 2.0));
if (numWindows <= c) {
numRows = numWindows;
numCols = 1;
} else {
numCols = Math.max(2, Math.round(numWindows / c));
numRows = Math.round(Math.ceil(numWindows / numCols));
}
} else {
// windows are much taller than display
c = Math.round(arDiff * 2);
if (numWindows <= c) {
numCols = numWindows;
numRows = 1;
} else {
numRows = Math.max(2, Math.round(numWindows / c));
numCols = Math.round(Math.ceil(numWindows / numRows));
}
}
numCells = numRows * numCols;
// determine the bounds of the tiling area
var titleBar = config.ui.titleBarHeight;
if (config.ui.auto_hide_ui === true) {
titleBar = 0;
}
var areaX = 0;
var areaY = Math.round(1.5 * titleBar); // keep 0.5 height as margin
if (config.ui.auto_hide_ui === true) {
areaY = -config.ui.titleBarHeight;
}
var areaW = config.totalWidth;
var areaH = config.totalHeight - (1.0 * titleBar);
var tileW = Math.floor(areaW / numCols);
var tileH = Math.floor(areaH / numRows);
var padding = 4;
// if only one application, no padding, i.e maximize
if (numWindows === 1) {
padding = 0;
}
var centroidsApps = {};
var centroidsTiles = [];
// Caculate apps centers
for (key in appsWithoutBackground) {
app = appsWithoutBackground[key];
centroidsApps[key] = {x: app.left + app.width / 2.0, y: app.top + app.height / 2.0};
}
// Caculate tiles centers
for (i = 0; i < numCells; i++) {
c = i % numCols;
r = Math.floor(i / numCols);
centroidsTiles.push({x: (c * tileW + areaX) + tileW / 2.0, y: (r * tileH + areaY) + tileH / 2.0});
}
// Calculate distances
var distances = {};
for (key in centroidsApps) {
distances[key] = [];
for (i = 0; i < numCells; i++) {
var d = distanceSquared2D(centroidsApps[key], centroidsTiles[i]);
distances[key].push(d);
}
}
stickyAppHandler.enablePiling = true;
for (key in appsWithoutBackground) {
// get the application
app = appsWithoutBackground[key];
// pick a cell
var cellid = findMinimum(distances[key]);
// put infinite value to disable the chosen cell
for (i in appsWithoutBackground) {
distances[i][cellid] = Number.MAX_VALUE;
}
// calculate new dimensions
c = cellid % numCols;
r = Math.floor(cellid / numCols);
var newdims = fitWithin(app, c * tileW + areaX, r * tileH + areaY, tileW, tileH, padding);
// update the data structure
app.left = newdims[0];
app.top = newdims[1] - titleBar;
app.width = newdims[2];
app.height = newdims[3];
var updateItem = {
elemId: app.id,
elemLeft: app.left,
elemTop: app.top,
elemWidth: app.width,
elemHeight: app.height,
force: true,
date: Date.now()
};
stickyAppHandler.pileItemsStickingToUpdatedItem(app);
broadcast('startMove', {id: updateItem.elemId, date: updateItem.date});
broadcast('startResize', {id: updateItem.elemId, date: updateItem.date});
moveAndResizeApplicationWindow(updateItem);
broadcast('finishedMove', {id: updateItem.elemId, date: updateItem.date});
broadcast('finishedResize', {id: updateItem.elemId, date: updateItem.date});
}
stickyAppHandler.enablePiling = false;
}
/**
* Remove all partitions and all applications
*
* @method clearDisplay
*/
function clearDisplay(wsio) {
deleteAllPartitions();
deleteAllApplications(wsio);
}
/**
* Close all the applications
*
* @method deleteAllApplications
*/
function deleteAllApplications(wsio) {
var i;
var all = Object.keys(SAGE2Items.applications.list);
for (i = 0; i < all.length; i++) {
deleteApplication(all[i], null, wsio);
}
// Reset the app_id counter to 0
getUniqueAppId(-1);
}
/**
* Remove all the partitions and keep the applications
*
* @method deleteAllPartitions
*/
function deleteAllPartitions() {
// delete all partitions
for (var key of Object.keys(partitions.list)) {
deletePartition(key);
}
// reset partition counter to 0
partitions.totalCreated = 0;
}
/**
* Remove all applications
*
* @method wsDeleteAllApplications
*/
function wsDeleteAllApplications(wsio) {
deleteAllApplications(wsio);
}
// handlers for messages from UI
function wsClearDisplay(wsio, data) {
clearDisplay(wsio);
addEventToUserLog(wsio.id, {type: "clearDisplay", data: null, time: Date.now()});
}
function wsTileApplications(wsio, data) {
tileApplications();
addEventToUserLog(wsio.id, {type: "tileApplications", data: null, time: Date.now()});
}
// ************** Server File Functions *****************
function wsRequestAvailableApplications(wsio, data) {
var apps = assets.listApps();
wsio.emit('availableApplications', apps);
}
function wsRequestStoredFiles(wsio, data) {
var savedFiles = getSavedFilesList();
wsio.emit('storedFileList', savedFiles);
}
function wsLoadApplication(wsio, data) {
// Check if the user can do that
if (!userlist.isAllowed(wsio.id, 'use apps')
&& (wsio.clientType !== "display")) {
if (wsio.emit) {
wsio.emit('cancelAction', 'application');
}
return;
}
var appData = {application: "custom_app", filename: data.application, data: data.data};
appLoader.loadFileFromLocalStorage(appData, function(appInstance) {
appInstance.id = getUniqueAppId();
if (appInstance.animation) {
var i;
SAGE2Items.renderSync[appInstance.id] = {clients: {}, date: Date.now()};
for (i = 0; i < clients.length; i++) {
if (clients[i].clientType === "display") {
SAGE2Items.renderSync[appInstance.id].clients[clients[i].id] = {
wsio: clients[i], readyForNextFrame: false, blocklist: []
};
}
}
}
// Get the drop position and convert it to wall coordinates
var position = data.position || [0, 0];
if (position[0] > 1) {
// value in pixels, used as origin
appInstance.left = position[0];
} else {
// value in percent
position[0] = Math.round(position[0] * config.totalWidth);
// Use the position as center of drop location
appInstance.left = position[0] - appInstance.width / 2;
if (appInstance.left < 0) {
appInstance.left = 0;
}
}
if (position[1] > 1) {
// value in pixels, used as origin
appInstance.top = position[1];
} else {
// value in percent
position[1] = Math.round(position[1] * config.totalHeight);
// Use the position as center of drop location
appInstance.top = position[1] - appInstance.height / 2;
if (appInstance.top < 0) {
appInstance.top = 0;
}
}
// Get the size if any specificed
var initialSize = data.dimensions;
if (initialSize) {
appInstance.width = initialSize[0];
appInstance.height = initialSize[1];
appInstance.aspect = initialSize[0] / initialSize[1];
}
/*
If this app is launched from launchAppWithValues command and the position isn't specified, then need to calculate
First check if it is the first app, they all start from the same place
If not the first, then check if the position of (last x + last width + padding + this width < wall width)
if fits, add to this row
if not fit, then check if fits on next row (last y + last tallest + padding + this height < wall height)
if fit, add to next row
if no fit, then restart
*/
if (data.wasLaunchedThroughMessage && !data.wasPositionGivenInMessage) {
let xApp, yApp;
// if this is the first app.
if (appLaunchPositioning.xLast === -1) {
xApp = appLaunchPositioning.xStart;
yApp = appLaunchPositioning.yStart;
} else {
// if not the first app, check that this app fits in the current row
let fit = false;
if (appLaunchPositioning.xLast + appLaunchPositioning.widthLast
+ appLaunchPositioning.padding + appInstance.width < config.totalWidth) {
if (appLaunchPositioning.yLast + appInstance.height < config.totalHeight) {
fit = true;
}
}
// if the app fits, then let use the modified position
if (fit) {
xApp = appLaunchPositioning.xLast + appLaunchPositioning.widthLast
+ appLaunchPositioning.padding;
yApp = appLaunchPositioning.yLast;
} else { // need to see if fits on next row or restart.
// either way changing row, set this app's height as tallest in row.
appLaunchPositioning.tallestInRow = appInstance.height;
// if fits on next row, put it there
if (appLaunchPositioning.yLast + appLaunchPositioning.tallestInRow
+ appLaunchPositioning.padding + appInstance.height < config.totalHeight) {
xApp = appLaunchPositioning.xStart;
yApp = appLaunchPositioning.yLast + appLaunchPositioning.tallestInRow
+ appLaunchPositioning.padding;
} else { // doesn't fit, restart
xApp = appLaunchPositioning.xStart;
yApp = appLaunchPositioning.yStart;
}
}
}
// set the app values
appInstance.left = xApp;
appInstance.top = yApp;
// track the values to position adjust next app
appLaunchPositioning.xLast = appInstance.left;
appLaunchPositioning.yLast = appInstance.top;
appLaunchPositioning.widthLast = appInstance.width;
appLaunchPositioning.heightLast = appInstance.height;
if (appInstance.height > appLaunchPositioning.tallestInRow) {
appLaunchPositioning.tallestInRow = appInstance.height;
}
}
// if supplied more values to init with
if (data.wasLaunchedThroughMessage && data.customLaunchParams) {
appInstance.customLaunchParams = data.customLaunchParams;
}
handleNewApplication(appInstance, null);
// By not deleting it will be given whenever display client refreshes/connect
// delete appInstance.customLaunchParams;
broadcast('userEvent', {type: 'load application', data: data, id: wsio.id});
addEventToUserLog(data.user, {type: "openApplication", data:
{application: {id: appInstance.id, type: appInstance.application}}, time: Date.now()});
});
}
function wsLoadImageFromBuffer(wsio, data) {
if (!userlist.isAllowed(wsio.id, 'upload files')) {
wsio.emit('cancelAction', 'file');
return;
}
appLoader.loadImageFromDataBuffer(data.src, data.width, data.height,
data.mime, "", data.url, data.title, {},
function(appInstance) {
// Get the drop position and convert it to wall coordinates
var position = data.position || [0, 0];
if (position[0] > 1) {
// value in pixels, used as origin
appInstance.left = position[0];
} else {
// value in percent
position[0] = Math.round(position[0] * config.totalWidth);
// Use the position as center of drop location
appInstance.left = position[0] - appInstance.width / 2;
if (appInstance.left < 0) {
appInstance.left = 0;
}
}
if (position[1] > 1) {
// value in pixels, used as origin
appInstance.top = position[1];
} else {
// value in percent
position[1] = Math.round(position[1] * config.totalHeight);
// Use the position as center of drop location
appInstance.top = position[1] - appInstance.height / 2;
if (appInstance.top < 0) {
appInstance.top = 0;
}
}
appInstance.id = getUniqueAppId();
handleNewApplication(appInstance, null);
broadcast('userEvent', {type: 'load file', data: data, id: wsio.id});
addEventToUserLog(data.user, {type: "openFile", data:
{name: data.filename, application: {id: appInstance.id, type: appInstance.application}}, time: Date.now()});
});
}
function wsLoadFileFromServer(wsio, data) {
if (!userlist.isAllowed(wsio.id, 'upload files')) {
wsio.emit('cancelAction', 'file');
return;
}
if (data.application === "load_session") {
// if it's a session, then load it
loadSession(data.filename);
broadcast('userEvent', {type: 'load file', data: data, id: wsio.id});
addEventToUserLog(wsio.id, {type: "openFile", data: {name: data.filename,
application: {id: null, type: "session"}}, time: Date.now()});
} else {
appLoader.loadFileFromLocalStorage(data, function(appInstance, videohandle) {
// Get the drop position and convert it to wall coordinates
var position = data.position || [0, 0];
if (position[0] > 1) {
// value in pixels, used as origin
appInstance.left = position[0];
} else {
// value in percent
position[0] = Math.round(position[0] * config.totalWidth);
// Use the position as center of drop location
appInstance.left = position[0] - appInstance.width / 2;
if (appInstance.left < 0) {
appInstance.left = 0;
}
}
if (position[1] > 1) {
// value in pixels, used as origin
appInstance.top = position[1];
} else {
// value in percent
position[1] = Math.round(position[1] * config.totalHeight);
// Use the position as center of drop location
appInstance.top = position[1] - appInstance.height / 2;
if (appInstance.top < 0) {
appInstance.top = 0;
}
}
appInstance.id = getUniqueAppId();
// Add the application in the list of renderSync if needed
if (appInstance.animation) {
var i;
SAGE2Items.renderSync[appInstance.id] = {clients: {}, date: Date.now()};
for (i = 0; i < clients.length; i++) {
if (clients[i].clientType === "display") {
SAGE2Items.renderSync[appInstance.id].clients[clients[i].id] = {
wsio: clients[i], readyForNextFrame: false, blocklist: []
};
}
}
}
handleNewApplication(appInstance, videohandle);
broadcast('userEvent', {type: 'load file', data: data, id: wsio.id});
addEventToUserLog(data.user, {type: "openFile", data:
{name: data.filename, application: {id: appInstance.id, type: appInstance.application}}, time: Date.now()});
});
}
}
function initializeLoadedVideo(appInstance, videohandle) {
if (appInstance.application !== "movie_player" || videohandle === null) {
return;
}
var i;
var horizontalBlocks = Math.ceil(appInstance.native_width / mediaBlockSize);
var verticalBlocks = Math.ceil(appInstance.native_height / mediaBlockSize);
var videoBuffer = new Array(horizontalBlocks * verticalBlocks);
videohandle.on('error', function(err) {
console.log("VIDEO ERROR: " + err);
});
videohandle.on('start', function() {
broadcast('videoPlaying', {id: appInstance.id});
});
videohandle.on('end', function() {
broadcast('videoEnded', {id: appInstance.id});
if (SAGE2Items.renderSync[appInstance.id].loop === true) {
SAGE2Items.renderSync[appInstance.id].decoder.seek(0.0, function() {
SAGE2Items.renderSync[appInstance.id].decoder.play();
});
broadcast('updateVideoItemTime', {id: appInstance.id, timestamp: 0.0, play: false});
}
});
videohandle.on('frame', function(frameIdx, buffer) {
SAGE2Items.renderSync[appInstance.id].frameIdx = frameIdx;
var blockBuffers = pixelblock.yuv420ToPixelBlocks(buffer,
appInstance.data.width, appInstance.data.height, mediaBlockSize);
var idBuffer = Buffer.concat([new Buffer(appInstance.id), new Buffer([0])]);
var frameIdxBuffer = intToByteBuffer(frameIdx, 4);
var dateBuffer = intToByteBuffer(Date.now(), 8);
for (i = 0; i < blockBuffers.length; i++) {
var blockIdxBuffer = intToByteBuffer(i, 2);
SAGE2Items.renderSync[appInstance.id].pixelbuffer[i] = Buffer.concat([idBuffer, blockIdxBuffer,
frameIdxBuffer, dateBuffer, blockBuffers[i]]);
}
handleNewVideoFrame(appInstance.id);
});
SAGE2Items.renderSync[appInstance.id] = {decoder: videohandle, frameIdx: null, loop: false,
pixelbuffer: videoBuffer, newFrameGenerated: false, clients: {}};
for (i = 0; i < clients.length; i++) {
if (clients[i].clientType === "display") {
SAGE2Items.renderSync[appInstance.id].clients[clients[i].id] = {
wsio: clients[i], readyForNextFrame: false, blocklist: []
};
}
}
calculateValidBlocks(appInstance, mediaBlockSize, SAGE2Items.renderSync[appInstance.id]);
// initialize based on state
SAGE2Items.renderSync[appInstance.id].loop = appInstance.data.looped;
if (appInstance.data.frame !== 0) {
var ts = appInstance.data.frame / appInstance.data.framerate;
SAGE2Items.renderSync[appInstance.id].decoder.seek(ts, function() {
if (appInstance.data.paused === false) {
SAGE2Items.renderSync[appInstance.id].decoder.play();
}
});
broadcast('updateVideoItemTime', {id: appInstance.id, timestamp: ts, play: false});
} else {
if (appInstance.data.paused === false) {
SAGE2Items.renderSync[appInstance.id].decoder.play();
}
}
if (appInstance.data.muted === true) {
broadcast('videoMuted', {id: appInstance.id});
}
}
// move this function elsewhere
function handleNewVideoFrame(id) {
var videohandle = SAGE2Items.renderSync[id];
videohandle.newFrameGenerated = true;
if (!allTrueDict(videohandle.clients, "readyForNextFrame")) {
return false;
}
updateVideoFrame(id);
return true;
}
// move this function elsewhere
function handleNewClientReady(id) {
var videohandle = SAGE2Items.renderSync[id];
// if no new frame is generate or not all display clients have finished rendering previous frame - return
if (videohandle.newFrameGenerated !== true || !allTrueDict(videohandle.clients, "readyForNextFrame")) {
return false;
}
updateVideoFrame(id);
return true;
}
function updateVideoFrame(id) {
var i;
var key;
var videohandle = SAGE2Items.renderSync[id];
videohandle.newFrameGenerated = false;
for (key in videohandle.clients) {
videohandle.clients[key].wsio.emit('updateFrameIndex', {id: id, frameIdx: videohandle.frameIdx});
var hasBlock = false;
for (i = 0; i < videohandle.pixelbuffer.length; i++) {
if (videohandle.clients[key].blocklist.indexOf(i) >= 0) {
hasBlock = true;
videohandle.clients[key].wsio.emit('updateVideoFrame', videohandle.pixelbuffer[i]);
}
}
if (hasBlock === true) {
videohandle.clients[key].readyForNextFrame = false;
}
}
}
// move this function elsewhere
function calculateValidBlocks(app, blockSize, renderhandle) {
if (app.application !== "movie_player" && app.application !== "media_block_stream") {
return;
}
var i;
var j;
var key;
var portalX = 0;
var portalY = 0;
var portalScale = 1;
var titleBarHeight = config.ui.titleBarHeight;
var portal = findApplicationPortal(app);
if (portal !== undefined && portal !== null) {
portalX = portal.data.left;
portalY = portal.data.top;
portalScale = portal.data.scale;
titleBarHeight = portal.data.titleBarHeight;
}
var horizontalBlocks = Math.ceil(app.data.width / blockSize);
var verticalBlocks = Math.ceil(app.data.height / blockSize);
var renderBlockWidth = (blockSize * app.width / app.data.width) * portalScale;
var renderBlockHeight = (blockSize * app.height / app.data.height) * portalScale;
for (key in renderhandle.clients) {
renderhandle.clients[key].blocklist = [];
for (i = 0; i < verticalBlocks; i++) {
for (j = 0; j < horizontalBlocks; j++) {
var blockIdx = i * horizontalBlocks + j;
if (renderhandle.clients[key].wsio.clientID < 0) {
renderhandle.clients[key].blocklist.push(blockIdx);
} else {
var display = config.displays[renderhandle.clients[key].wsio.clientID];
var left = j * renderBlockWidth + (app.left * portalScale + portalX);
var top = i * renderBlockHeight + ((app.top + titleBarHeight) * portalScale + portalY);
var offsetX = config.resolution.width * display.column;
var offsetY = config.resolution.height * display.row;
if ((left + renderBlockWidth) >= offsetX &&
left <= (offsetX + config.resolution.width * display.width) &&
(top + renderBlockHeight) >= offsetY &&
top <= (offsetY + config.resolution.height * display.height)) {
renderhandle.clients[key].blocklist.push(blockIdx);
}
}
}
}
renderhandle.clients[key].wsio.emit('updateValidStreamBlocks', {
id: app.id, blockList: renderhandle.clients[key].blocklist
});
}
}
function wsDeleteElementFromStoredFiles(wsio, data) {
if (data.application === "sage2/session") {
// if it's a session
deleteSession(data.filename, function() {
// send the update file list
broadcast('storedFileList', getSavedFilesList());
});
} else {
assets.deleteAsset(data.filename, function(err) {
if (!err) {
// send the update file list
broadcast('storedFileList', getSavedFilesList());
}
});
}
}
function wsMoveElementFromStoredFiles(wsio, data) {
var destinationURL = data.url;
var destinationFile;
// calculate the new destination filename
for (var folder in mediaFolders) {
var f = mediaFolders[folder];
if (destinationURL.indexOf(f.url) === 0) {
var splits = destinationURL.split(f.url);
var subdir = splits[1];
destinationFile = path.join(f.path, subdir, path.basename(data.filename));
}
}
// Do the move and reprocess the asset
if (destinationFile) {
assets.moveAsset(data.filename, destinationFile, function(err) {
if (err) {
sageutils.log('Assets', 'Error moving', data.filename);
} else {
// if all good, send the new list of files
// wsRequestStoredFiles(wsio);
// send the update file list
broadcast('storedFileList', getSavedFilesList());
}
});
}
}
// ************** Adding Web Content (URL) *****************
function wsAddNewWebElement(wsio, data) {
if (data.type === "application/url") {
if (!userlist.isAllowed(wsio.id, 'use apps')) {
wsio.emit('cancelAction', 'application');
return;
}
} else {
if (!userlist.isAllowed(wsio.id, 'upload files')) {
wsio.emit('cancelAction', 'file');
return;
}
}
appLoader.loadFileFromWebURL(data, function(appInstance, videohandle) {
// Update the file list for the UI clients
broadcast('storedFileList', getSavedFilesList());
// Get the drop position and convert it to wall coordinates
var position = data.position || [0, 0];
position[0] = Math.round(position[0] * config.totalWidth);
position[1] = Math.round(position[1] * config.totalHeight);
// Use the position from the drop location
if (position[0] !== 0 || position[1] !== 0) {
appInstance.left = position[0] - appInstance.width / 2;
if (appInstance.left < 0) {
appInstance.left = 0;
}
appInstance.top = position[1] - appInstance.height / 2;
if (appInstance.top < 0) {
appInstance.top = 0;
}
}
appInstance.id = getUniqueAppId();
handleNewApplication(appInstance, videohandle);
if (appInstance.animation) {
var i;
SAGE2Items.renderSync[appInstance.id] = {clients: {}, date: Date.now()};
for (i = 0; i < clients.length; i++) {
if (clients[i].clientType === "display") {
SAGE2Items.renderSync[appInstance.id].clients[clients[i].id] = {
wsio: clients[i], readyForNextFrame: false, blocklist: []
};
}
}
}
});
}
// ************** Folder management *****************
function wsCreateFolder(wsio, data) {
// Create a folder as needed
for (var folder in mediaFolders) {
var f = mediaFolders[folder];
// if it starts with the sage root
if (data.root.indexOf(f.url) === 0) {
var subdir = data.root.split(f.url)[1];
var toCreate = path.join(f.path, subdir, data.path);
if (!sageutils.folderExists(toCreate)) {
sageutils.mkdirParent(toCreate);
sageutils.log("Folders", toCreate, 'created');
}
}
}
}
// ************** Command line *****************
function wsCommand(wsio, data) {
// send the command to the REPL interpreter
processInputCommand(data);
}
// ************** Launching Web Browser *****************
function wsOpenNewWebpage(wsio, data) {
sageutils.log('Webview', "opening", data.url);
wsLoadApplication(wsio, {
application: "/uploads/apps/Webview",
user: wsio.id,
// pass the url in the data object
data: data,
position: [0, 0]
});
// Check if the web-browser is connected
if (webBrowserClient !== null) {
// then emit the command
console.log("Browser> new page", data.url);
webBrowserClient.emit('openWebBrowser', {url: data.url});
}
}
// ************** Volume sync ********************
function wsSetVolume(wsio, data) {
if (SAGE2Items.renderSync[data.id] === undefined || SAGE2Items.renderSync[data.id] === null) {
return;
}
broadcast('setVolume', data);
}
// ************** Video / Audio Synchonization *****************
function wsPlayVideo(wsio, data) {
if (SAGE2Items.renderSync[data.id] === undefined || SAGE2Items.renderSync[data.id] === null) {
return;
}
SAGE2Items.renderSync[data.id].decoder.play();
}
function wsPauseVideo(wsio, data) {
if (SAGE2Items.renderSync[data.id] === undefined || SAGE2Items.renderSync[data.id] === null) {
return;
}
SAGE2Items.renderSync[data.id].decoder.pause(function() {
broadcast('videoPaused', {id: data.id});
});
}
function wsStopVideo(wsio, data) {
if (SAGE2Items.renderSync[data.id] === undefined || SAGE2Items.renderSync[data.id] === null) {
return;
}
SAGE2Items.renderSync[data.id].decoder.stop(function() {
broadcast('videoPaused', {id: data.id});
broadcast('updateVideoItemTime', {id: data.id, timestamp: 0.0, play: false});
broadcast('updateFrameIndex', {id: data.id, frameIdx: 0});
});
}
function wsUpdateVideoTime(wsio, data) {
if (SAGE2Items.renderSync[data.id] === undefined || SAGE2Items.renderSync[data.id] === null) {
return;
}
SAGE2Items.renderSync[data.id].decoder.seek(data.timestamp, function() {
if (data.play === true) {
SAGE2Items.renderSync[data.id].decoder.play();
}
});
broadcast('updateVideoItemTime', data);
}
function wsMuteVideo(wsio, data) {
if (SAGE2Items.renderSync[data.id] === undefined || SAGE2Items.renderSync[data.id] === null) {
return;
}
broadcast('videoMuted', {id: data.id});
}
function wsUnmuteVideo(wsio, data) {
if (SAGE2Items.renderSync[data.id] === undefined || SAGE2Items.renderSync[data.id] === null) {
return;
}
broadcast('videoUnmuted', {id: data.id});
}
function wsLoopVideo(wsio, data) {
if (SAGE2Items.renderSync[data.id] === undefined || SAGE2Items.renderSync[data.id] === null) {
return;
}
SAGE2Items.renderSync[data.id].loop = data.loop;
}
// ************** Remote Server Content *****************
function wsAddNewElementFromRemoteServer(wsio, data) {
console.log("add element from remote server");
var i;
appLoader.loadApplicationFromRemoteServer(data, function(appInstance, videohandle) {
console.log("Remote App: " + appInstance.title + " (" + appInstance.application + ")");
if (appInstance.application === "media_stream" || appInstance.application === "media_block_stream") {
appInstance.id = wsio.remoteAddress.address + ":" + wsio.remoteAddress.port + "|" + appInstance.id;
SAGE2Items.renderSync[appInstance.id] = {chunks: [], clients: {}};
for (i = 0; i < clients.length; i++) {
if (clients[i].clientType === "display") {
SAGE2Items.renderSync[appInstance.id].clients[clients[i].id] = {
wsio: clients[i], readyForNextFrame: false, blocklist: []
};
}
}
} else {
appInstance.id = getUniqueAppId();
}
sageutils.mergeObjects(data.data, appInstance.data, ['video_url', 'video_type', 'audio_url', 'audio_type']);
handleNewApplication(appInstance, videohandle);
if (appInstance.animation) {
SAGE2Items.renderSync[appInstance.id] = {clients: {}, date: Date.now()};
for (i = 0; i < clients.length; i++) {
if (clients[i].clientType === "display") {
SAGE2Items.renderSync[appInstance.id].clients[clients[i].id] = {
wsio: clients[i], readyForNextFrame: false, blocklist: []
};
}
}
}
});
}
function wsAddNewSharedElementFromRemoteServer(wsio, data) {
var i;
appLoader.loadApplicationFromRemoteServer(data.application, function(appInstance, videohandle) {
sageutils.log("Remote App", appInstance.title + " (" + appInstance.application + ")");
if (appInstance.application === "media_stream" || appInstance.application === "media_block_stream") {
appInstance.id = wsio.remoteAddress.address + ":" + wsio.remoteAddress.port + "|" + data.id;
SAGE2Items.renderSync[appInstance.id] = {chunks: [], clients: {}};
for (i = 0; i < clients.length; i++) {
if (clients[i].clientType === "display") {
sageutils.log("Remote App", "render client", clients[i].id);
SAGE2Items.renderSync[appInstance.id].clients[clients[i].id] = {
wsio: clients[i], readyForNextFrame: false, blocklist: []
};
}
}
} else {
appInstance.id = data.id;
}
sageutils.mergeObjects(data.application.data, appInstance.data, ['video_url', 'video_type', 'audio_url', 'audio_type']);
handleNewApplication(appInstance, videohandle);
if (appInstance.animation) {
SAGE2Items.renderSync[appInstance.id] = {clients: {}, date: Date.now()};
for (i = 0; i < clients.length; i++) {
if (clients[i].clientType === "display") {
SAGE2Items.renderSync[appInstance.id].clients[clients[i].id] = {
wsio: clients[i], readyForNextFrame: false, blocklist: []
};
}
}
}
sharedApps[appInstance.id] = [{wsio: wsio, sharedId: data.remoteAppId}];
SAGE2Items.applications.editButtonVisibilityOnItem(appInstance.id, "syncButton", true);
broadcast('setAppSharingFlag', {id: appInstance.id, sharing: true});
});
}
function wsRequestNextRemoteFrame(wsio, data) {
var originId;
var portalCloneIdx = data.id.indexOf("_");
if (portalCloneIdx >= 0) {
originId = data.id.substring(0, portalCloneIdx);
} else {
originId = data.id;
}
var remote_id = config.host + ":" + config.secure_port + "|" + data.id;
if (SAGE2Items.applications.list.hasOwnProperty(originId)) {
var stream = SAGE2Items.applications.list[originId];
wsio.emit('updateRemoteMediaStreamFrame', {id: remote_id, state: stream.data});
} else {
wsio.emit('stopMediaStream', {id: remote_id});
}
}
function wsUpdateRemoteMediaStreamFrame(wsio, data) {
if (!SAGE2Items.applications.list.hasOwnProperty(data.id)) {
return;
}
var key;
for (key in SAGE2Items.renderSync[data.id].clients) {
SAGE2Items.renderSync[data.id].clients[key].readyForNextFrame = false;
}
var stream = SAGE2Items.applications.list[data.id];
stream.data = data.data;
broadcast('updateMediaStreamFrame', data);
}
function wsReceivedRemoteMediaStreamFrame(wsio, data) {
SAGE2Items.renderSync[data.id].clients[wsio.id].readyForNextFrame = true;
if (allTrueDict(SAGE2Items.renderSync[data.id].clients, "readyForNextFrame")) {
var i;
var mediaStreamData = data.id.substring(6).split("|");
var sender = {wsio: null, serverId: mediaStreamData[0], clientId: mediaStreamData[1], streamId: null};
for (i = 0; i < clients.length; i++) {
if (clients[i].id === sender.serverId) {
sender.wsio = clients[i];
break;
}
}
if (sender.wsio !== null) {
sender.wsio.emit('requestNextRemoteFrame', {id: sender.clientId});
}
}
}
// XXX - Remote block streaming not tested
function wsRequestNextRemoteBlockFrame(wsio, data) {
var remote_id = config.host + ":" + config.secure_port + "|" + data.id;
if (SAGE2Items.applications.list.hasOwnProperty(data.id)) {
var stream = SAGE2Items.applications.list[data.id];
wsio.emit('updateRemoteMediaBlockStreamFrame', {id: remote_id, state: stream.data});
} else {
wsio.emit('stopMediaBlockStream', {id: remote_id});
}
}
function wsUpdateRemoteMediaBlockStreamFrame(wsio, data) {
if (!SAGE2Items.applications.list.hasOwnProperty(data.id)) {
return;
}
var key;
for (key in SAGE2Items.renderSync[data.id].clients) {
SAGE2Items.renderSync[data.id].clients[key].readyForNextFrame = false;
}
var stream = SAGE2Items.applications.list[data.id];
stream.data = data.data;
broadcast('updateMediaBlockStreamFrame', data);
}
function wsReceivedRemoteMediaBlockStreamFrame(wsio, data) {
SAGE2Items.renderSync[data.id].clients[wsio.id].readyForNextFrame = true;
if (allTrueDict(SAGE2Items.renderSync[data.id].clients, "readyForNextFrame")) {
var i;
var mediaBlockStreamData = data.id.substring(6).split("|");
var sender = {wsio: null, serverId: mediaBlockStreamData[0], clientId: mediaBlockStreamData[1], streamId: null};
for (i = 0; i < clients.length; i++) {
if (clients[i].id === sender.serverId) {
sender.wsio = clients[i];
break;
}
}
if (sender.wsio !== null) {
sender.wsio.emit('requestNextRemoteFrame', {id: sender.clientId});
}
}
}
function wsRequestDataSharingSession(wsio, data) {
var known_site = findRemoteSiteByConnection(wsio);
if (known_site !== null) {
data.config.name = known_site.name;
}
if (data.config.name === undefined || data.config.name === null) {
data.config.name = "Unknown";
}
console.log("Data-sharing request from " + data.config.name + " (" + data.config.host + ":" + data.config.secure_port + ")");
broadcast('requestedDataSharingSession', {name: data.config.name, host: data.config.host, port: data.config.port});
// remoteSharingRequestDialog = {wsio: wsio, config: data.config};
showRequestDialog(true);
}
function wsCancelDataSharingSession(wsio, data) {
console.log("Data-sharing request cancelled");
broadcast('closeRequestDataSharingDialog', null, 'requiresFullApps');
// remoteSharingRequestDialog = null;
showRequestDialog(false);
}
function wsAcceptDataSharingSession(wsio, data) {
var myMin = Math.min(config.totalWidth, config.totalHeight - config.ui.titleBarHeight);
var sharingScale = (0.9 * myMin) / Math.min(data.width, data.height);
console.log("Data-sharing request accepted: " + data.width + "x" + data.height + ", scale: " + sharingScale);
broadcast('closeDataSharingWaitDialog', null);
createNewDataSharingSession(remoteSharingWaitDialog.name, remoteSharingWaitDialog.wsio.remoteAddress.address,
remoteSharingWaitDialog.wsio.remoteAddress.port, remoteSharingWaitDialog.wsio,
new Date(data.date), data.width, data.height, sharingScale, data.titleBarHeight, true);
remoteSharingWaitDialog = null;
showWaitDialog(false);
}
function wsRejectDataSharingSession(wsio, data) {
console.log("Data-sharing request rejected");
broadcast('closeDataSharingWaitDialog', null, 'requiresFullApps');
remoteSharingWaitDialog = null;
showWaitDialog(false);
}
function wsCreateRemoteSagePointer(wsio, data) {
var key;
var portalId = null;
for (key in remoteSharingSessions) {
if (remoteSharingSessions[key].portal.host === data.portal.host &&
remoteSharingSessions[key].portal.port === data.portal.port) {
portalId = key;
}
}
createSagePointer(data.id, portalId);
}
function wsStartRemoteSagePointer(wsio, data) {
sagePointers[data.id].left = data.left;
sagePointers[data.id].top = data.top;
showPointer(data.id, data);
}
function wsStopRemoteSagePointer(wsio, data) {
hidePointer(data.id, data);
// return to window interaction mode after stopping pointer
if (remoteInteraction[data.id].appInteractionMode()) {
remoteInteraction[data.id].toggleModes();
broadcast('changeSagePointerMode', {id: sagePointers[data.id].id, mode: remoteInteraction[data.id].interactionMode });
}
}
function wsRecordInnerGeometryForWidget(wsio, data) {
// var center = data.innerGeometry.center;
var buttons = data.innerGeometry.buttons;
var textInputs = data.innerGeometry.textInputs;
var sliders = data.innerGeometry.sliders;
var radioButtons = data.innerGeometry.radioButtons;
// SAGE2Items.widgets.addButtonToItem(data.instanceID, "center", "circle", {x:center.x, y: center.y, r:center.r}, 0);
var i;
for (i = 0; i < buttons.length; i++) {
SAGE2Items.widgets.addButtonToItem(data.instanceID, buttons[i].id, "circle",
{x: buttons[i].x, y: buttons[i].y, r: buttons[i].r}, 0);
}
for (i = 0; i < textInputs.length; i++) {
SAGE2Items.widgets.addButtonToItem(data.instanceID, textInputs[i].id, "rectangle",
{x: textInputs[i].x, y: textInputs[i].y, w: textInputs[i].w, h: textInputs[i].h}, 0);
}
for (i = 0; i < sliders.length; i++) {
SAGE2Items.widgets.addButtonToItem(data.instanceID, sliders[i].id, "rectangle",
{x: sliders[i].x, y: sliders[i].y, w: sliders[i].w, h: sliders[i].h}, 0);
}
for (i = 0; i < radioButtons.length; i++) {
var radioOptions = radioButtons[i];
for (var j = 0; j < radioOptions.length; j++) {
SAGE2Items.widgets.addButtonToItem(data.instanceID, radioOptions[j].id, "circle",
{x: radioOptions[j].x, y: radioOptions[j].y, r: radioOptions[j].r}, 0);
}
}
}
function wsCreateAppClone(wsio, data) {
var app = SAGE2Items.applications.list[data.id];
createAppFromDescription(app, function(appInstance, videohandle) {
appInstance.id = getUniqueAppId();
if (appInstance.animation) {
var i;
SAGE2Items.renderSync[appInstance.id] = {clients: {}, date: Date.now()};
for (i = 0; i < clients.length; i++) {
if (clients[i].clientType === "display") {
SAGE2Items.renderSync[appInstance.id].clients[clients[i].id] = {
wsio: clients[i], readyForNextFrame: false, blocklist: []
};
}
}
}
handleNewApplication(appInstance, videohandle);
});
}
function wsRemoteSagePointerPosition(wsio, data) {
if (sagePointers[data.id] === undefined) {
return;
}
sagePointers[data.id].left = data.left;
sagePointers[data.id].top = data.top;
broadcast('updateSagePointerPosition', sagePointers[data.id]);
}
function wsRemoteSagePointerToggleModes(wsio, data) {
// remoteInteraction[data.id].toggleModes();
remoteInteraction[data.id].interactionMode = data.mode;
broadcast('changeSagePointerMode', {id: sagePointers[data.id].id, mode: remoteInteraction[data.id].interactionMode});
}
function wsRemoteSagePointerHoverCorner(wsio, data) {
var appId = data.appHoverCorner.elemId;
var app = null;
if (SAGE2Items.applications.list.hasOwnProperty(appId)) {
app = SAGE2Items.applications.list[appId];
} else if (SAGE2Items.applications.list.hasOwnProperty(wsio.id + "|" + appId)) {
data.appHoverCorner.elemId = wsio.id + "|" + appId;
appId = data.appHoverCorner.elemId;
app = SAGE2Items.applications.list[appId];
}
if (app === undefined || app === null) {
return;
}
broadcast('hoverOverItemCorner', data.appHoverCorner);
}
function wsAddNewRemoteElementInDataSharingPortal(wsio, data) {
var key;
var remote = null;
for (key in remoteSharingSessions) {
if (remoteSharingSessions[key].wsio.id === wsio.id) {
remote = remoteSharingSessions[key];
break;
}
}
console.log("adding element from remote server:");
if (remote !== null) {
createAppFromDescription(data, function(appInstance, videohandle) {
if (appInstance.application === "media_stream" || appInstance.application === "media_block_stream") {
appInstance.id = wsio.remoteAddress.address + ":" + wsio.remoteAddress.port + "|" + data.id;
} else {
appInstance.id = data.id;
}
appInstance.left = data.left;
appInstance.top = data.top;
appInstance.width = data.width;
appInstance.height = data.height;
remoteSharingSessions[remote.portal.id].appCount++;
var i;
SAGE2Items.renderSync[appInstance.id] = {clients: {}, date: Date.now()};
for (i = 0; i < clients.length; i++) {
if (clients[i].clientType === "display") {
SAGE2Items.renderSync[appInstance.id].clients[clients[i].id] = {
wsio: clients[i], readyForNextFrame: false, blocklist: []
};
}
}
handleNewApplicationInDataSharingPortal(appInstance, videohandle, remote.portal.id);
});
}
}
function wsUpdateApplicationOrder(wsio, data) {
// should check timestamp first (data.date)
broadcast('updateItemOrder', data.order);
}
function wsStartApplicationMove(wsio, data) {
// should check timestamp first (data.date)
var app = null;
if (SAGE2Items.applications.list.hasOwnProperty(data.appId)) {
app = SAGE2Items.applications.list[data.appId];
} else if (SAGE2Items.applications.list.hasOwnProperty(wsio.id + "|" + data.appId)) {
data.appId = wsio.id + "|" + data.appId;
app = SAGE2Items.applications.list[data.appId];
}
if (app === undefined || app === null) {
return;
}
broadcast('startMove', {id: data.appId, date: Date.now()});
var eLogData = {
type: "move",
action: "start",
application: {
id: app.id,
type: app.application
},
location: {
x: parseInt(app.left, 10),
y: parseInt(app.top, 10),
width: parseInt(app.width, 10),
height: parseInt(app.height, 10)
}
};
addEventToUserLog(data.id, {type: "windowManagement", data: eLogData, time: Date.now()});
}
function wsStartApplicationResize(wsio, data) {
// should check timestamp first (data.date)
var app = null;
if (SAGE2Items.applications.list.hasOwnProperty(data.appId)) {
app = SAGE2Items.applications.list[data.appId];
} else if (SAGE2Items.applications.list.hasOwnProperty(wsio.id + "|" + data.appId)) {
data.appId = wsio.id + "|" + data.appId;
app = SAGE2Items.applications.list[data.appId];
}
if (app === undefined || app === null) {
return;
}
broadcast('startResize', {id: data.appId, date: Date.now()});
var eLogData = {
type: "resize",
action: "start",
application: {
id: app.id,
type: app.application
},
location: {
x: parseInt(app.left, 10),
y: parseInt(app.top, 10),
width: parseInt(app.width, 10),
height: parseInt(app.height, 10)
}
};
addEventToUserLog(data.id, {type: "windowManagement", data: eLogData, time: Date.now()});
}
function wsUpdateApplicationPosition(wsio, data) {
// should check timestamp first (data.date)
var appId = data.appPositionAndSize.elemId;
var app = null;
if (SAGE2Items.applications.list.hasOwnProperty(appId)) {
app = SAGE2Items.applications.list[appId];
} else if (SAGE2Items.applications.list.hasOwnProperty(wsio.id + "|" + appId)) {
data.appPositionAndSize.elemId = wsio.id + "|" + appId;
appId = data.appPositionAndSize.elemId;
app = SAGE2Items.applications.list[appId];
}
if (app === undefined || app === null) {
return;
}
var titleBarHeight = config.ui.titleBarHeight;
if (data.portalId !== undefined && data.portalId !== null) {
titleBarHeight = remoteSharingSessions[data.portalId].portal.titleBarHeight;
}
app.left = data.appPositionAndSize.elemLeft;
app.top = data.appPositionAndSize.elemTop;
app.width = data.appPositionAndSize.elemWidth;
app.height = data.appPositionAndSize.elemHeight;
var im = findInteractableManager(data.appPositionAndSize.elemId);
im.editGeometry(app.id, "applications", "rectangle", {x: app.left, y: app.top, w: app.width, h: app.height + titleBarHeight});
broadcast('setItemPosition', data.appPositionAndSize);
if (SAGE2Items.renderSync.hasOwnProperty(app.id)) {
calculateValidBlocks(app, mediaBlockSize, SAGE2Items.renderSync[app.id]);
if (app.id in SAGE2Items.renderSync && SAGE2Items.renderSync[app.id].newFrameGenerated === false) {
handleNewVideoFrame(app.id);
}
}
}
function wsUpdateApplicationPositionAndSize(wsio, data) {
// should check timestamp first (data.date)
var appId = data.appPositionAndSize.elemId;
var app = null;
if (SAGE2Items.applications.list.hasOwnProperty(appId)) {
app = SAGE2Items.applications.list[appId];
} else if (SAGE2Items.applications.list.hasOwnProperty(wsio.id + "|" + appId)) {
data.appPositionAndSize.elemId = wsio.id + "|" + appId;
appId = data.appPositionAndSize.elemId;
app = SAGE2Items.applications.list[appId];
}
if (app === undefined || app === null) {
return;
}
var titleBarHeight = config.ui.titleBarHeight;
if (data.portalId !== undefined && data.portalId !== null) {
titleBarHeight = remoteSharingSessions[data.portalId].portal.titleBarHeight;
}
app.left = data.appPositionAndSize.elemLeft;
app.top = data.appPositionAndSize.elemTop;
app.width = data.appPositionAndSize.elemWidth;
app.height = data.appPositionAndSize.elemHeight;
var im = findInteractableManager(data.appPositionAndSize.elemId);
im.editGeometry(app.id, "applications", "rectangle", {x: app.left, y: app.top, w: app.width, h: app.height + titleBarHeight});
handleApplicationResize(app.id);
broadcast('setItemPositionAndSize', data.appPositionAndSize);
if (SAGE2Items.renderSync.hasOwnProperty(app.id)) {
calculateValidBlocks(app, mediaBlockSize, SAGE2Items.renderSync[app.id]);
if (app.id in SAGE2Items.renderSync && SAGE2Items.renderSync[app.id].newFrameGenerated === false) {
handleNewVideoFrame(app.id);
}
}
}
function wsFinishApplicationMove(wsio, data) {
// should check timestamp first (data.date)
var app = null;
if (SAGE2Items.applications.list.hasOwnProperty(data.appId)) {
app = SAGE2Items.applications.list[data.appId];
} else if (SAGE2Items.applications.list.hasOwnProperty(wsio.id + "|" + data.appId)) {
data.appId = wsio.id + "|" + data.appId;
app = SAGE2Items.applications.list[data.appId];
}
if (app === undefined || app === null) {
return;
}
broadcast('finishedMove', {id: data.appId, date: Date.now()});
var eLogData = {
type: "move",
action: "end",
application: {
id: app.id,
type: app.application
},
location: {
x: parseInt(app.left, 10),
y: parseInt(app.top, 10),
width: parseInt(app.width, 10),
height: parseInt(app.height, 10)
}
};
addEventToUserLog(data.id, {type: "windowManagement", data: eLogData, time: Date.now()});
}
function wsFinishApplicationResize(wsio, data) {
// should check timestamp first (data.date)
var app = null;
if (SAGE2Items.applications.list.hasOwnProperty(data.appId)) {
app = SAGE2Items.applications.list[data.appId];
} else if (SAGE2Items.applications.list.hasOwnProperty(wsio.id + "|" + data.appId)) {
data.appId = wsio.id + "|" + data.appId;
app = SAGE2Items.applications.list[data.appId];
}
if (app === undefined || app === null) {
return;
}
broadcast('finishedResize', {id: data.appId, date: Date.now()});
var eLogData = {
type: "resize",
action: "end",
application: {
id: app.id,
type: app.application
},
location: {
x: parseInt(app.left, 10),
y: parseInt(app.top, 10),
width: parseInt(app.width, 10),
height: parseInt(app.height, 10)
}
};
addEventToUserLog(data.id, {type: "windowManagement", data: eLogData, time: Date.now()});
}
function wsDeleteApplication(wsio, data) {
deleteApplication(data.appId, null, wsio);
// Is that diffent ?
// if (SAGE2Items.applications.list.hasOwnProperty(data.appId)) {
// SAGE2Items.applications.removeItem(data.appId);
// var im = findInteractableManager(data.appId);
// im.removeGeometry(data.appId, "applications");
// broadcast('deleteElement', {elemId: data.appId});
// }
}
function wsUpdateApplicationState(wsio, data) {
// should check timestamp first (data.date)
if (SAGE2Items.applications.list.hasOwnProperty(data.id)) {
var app = SAGE2Items.applications.list[data.id];
// hang on to old values if movie player
var oldTs;
var oldPaused;
var oldMuted;
if (app.application === "movie_player") {
oldTs = app.data.frame / app.data.framerate;
oldPaused = app.data.paused;
oldMuted = app.data.muted;
}
var modified = sageutils.mergeObjects(data.state, app.data,
['doc_url', 'video_url', 'video_type', 'audio_url', 'audio_type']);
if (modified === true) {
// update video demuxer based on state
if (app.application === "movie_player") {
console.log("received state from remote site:", data.state);
SAGE2Items.renderSync[app.id].loop = app.data.looped;
var ts = app.data.frame / app.data.framerate;
if (app.data.paused === true && ts !== oldTs) {
SAGE2Items.renderSync[app.id].decoder.seek(ts, function() {
// do nothing
});
broadcast('updateVideoItemTime', {id: app.id, timestamp: ts, play: false});
} else {
if (app.data.paused === true && oldPaused === false) {
SAGE2Items.renderSync[app.id].decoder.pause(function() {
broadcast('videoPaused', {id: app.id});
});
}
if (app.data.paused === false && oldPaused === true) {
SAGE2Items.renderSync[app.id].decoder.play();
}
}
if (app.data.muted === true && oldMuted === false) {
broadcast('videoMuted', {id: app.id});
}
if (app.data.muted === false && oldMuted === true) {
broadcast('videoUnmuted', {id: app.id});
}
}
// for all apps - send new state to app
broadcast('loadApplicationState', {id: app.id, state: app.data, date: Date.now()});
}
}
}
function wsUpdateApplicationStateOptions(wsio, data) {
// should check timestamp first (data.date)
if (SAGE2Items.applications.list.hasOwnProperty(data.id)) {
broadcast('loadApplicationOptions', {id: data.id, options: data.options});
}
}
// ************** Widget Control Messages *****************
function wsAddNewControl(wsio, data) {
if (!SAGE2Items.applications.list.hasOwnProperty(data.appId)) {
return;
}
if (SAGE2Items.widgets.list.hasOwnProperty(data.id)) {
return;
}
broadcast('createControl', data);
var zIndex = SAGE2Items.widgets.numItems;
var radialGeometry = {
x: data.left + (data.height / 2),
y: data.top + (data.height / 2),
r: data.height / 2
};
if (data.hasSideBar === true) {
var shapeData = {
radial: {
type: "circle",
visible: true,
geometry: radialGeometry
},
sidebar: {
type: "rectangle",
visible: true,
geometry: {
x: data.left + data.height,
y: data.top + (data.height / 2) - (data.barHeight / 2),
w: data.width - data.height, h: data.barHeight
}
}
};
interactMgr.addComplexGeometry(data.id, "widgets", shapeData, zIndex, data);
} else {
interactMgr.addGeometry(data.id, "widgets", "circle", radialGeometry, true, zIndex, data);
}
SAGE2Items.widgets.addItem(data);
var uniqueID = data.id.substring(data.appId.length, data.id.lastIndexOf("_"));
var app = SAGE2Items.applications.list[data.appId];
addEventToUserLog(uniqueID, {type: "widgetMenu", data: {action: "open", application:
{id: app.id, type: app.application}}, time: Date.now()});
}
function wsCloseAppFromControl(wsio, data) {
deleteApplication(data.appId, null, wsio);
}
function wsHideWidgetFromControl(wsio, data) {
var ctrl = SAGE2Items.widgets.list[data.instanceID];
hideControl(ctrl);
}
function wsOpenRadialMenuFromControl(wsio, data) {
console.log("radial menu");
var ctrl = SAGE2Items.widgets.list[data.id];
createRadialMenu(wsio.id, ctrl.left, ctrl.top);
}
function loadConfiguration() {
var configFile = null;
if (program.configuration) {
configFile = program.configuration;
} else {
// Read config.txt - if exists and specifies a user defined config, then use it
if (sageutils.fileExists("config.txt")) {
var lines = fs.readFileSync("config.txt", 'utf8').split("\n");
for (var i = 0; i < lines.length; i++) {
var text = "";
var comment = lines[i].indexOf("//");
if (comment >= 0) {
text = lines[i].substring(0, comment).trim();
} else {
text = lines[i].trim();
}
if (text !== "") {
configFile = text;
sageutils.log("SAGE2", "Found configuration file:", chalk.yellow.bold(configFile));
break;
}
}
}
}
// If config.txt does not exist or does not specify any files, look for a config with the hostname
if (configFile === null) {
var hn = os.hostname();
var dot = hn.indexOf(".");
if (dot >= 0) {
hn = hn.substring(0, dot);
}
configFile = path.join("config", hn + "-cfg.json");
if (sageutils.fileExists(configFile)) {
sageutils.log("SAGE2", "Found configuration file:", chalk.yellow.bold(configFile));
} else {
// Check in ~/Document/SAGE2_Media/config
if (platform === "Windows") {
configFile = path.join(mainFolder.path, "config", "defaultWin-cfg.json");
} else {
configFile = path.join(mainFolder.path, "config", "default-cfg.json");
}
// finally check in the internal folder
if (!sageutils.fileExists(configFile)) {
if (platform === "Windows") {
configFile = path.join("config", "defaultWin-cfg.json");
} else {
configFile = path.join("config", "default-cfg.json");
}
}
sageutils.log("SAGE2", "Using default configuration file:", chalk.yellow.bold(configFile));
}
}
if (!sageutils.fileExists(configFile)) {
console.log("\n----------");
console.log("Cannot find configuration file:", configFile);
console.log("----------\n\n");
process.exit(1);
}
// Read the specified configuration file
var json_str = fs.readFileSync(configFile, 'utf8');
// Parse it using JSON5 syntax (more lax than strict JSON)
var userConfig = json5.parse(json_str);
// compute extra dependent parameters
userConfig.totalWidth = userConfig.resolution.width * userConfig.layout.columns;
userConfig.totalHeight = userConfig.resolution.height * userConfig.layout.rows;
var minDim = Math.min(userConfig.totalWidth, userConfig.totalHeight);
var maxDim = Math.max(userConfig.totalWidth, userConfig.totalHeight);
if (userConfig.ui.titleBarHeight) {
userConfig.ui.titleBarHeight = parseInt(userConfig.ui.titleBarHeight, 10);
} else {
userConfig.ui.titleBarHeight = Math.round(0.025 * minDim);
}
if (userConfig.ui.widgetControlSize) {
userConfig.ui.widgetControlSize = parseInt(userConfig.ui.widgetControlSize, 10);
} else {
userConfig.ui.widgetControlSize = Math.round(0.020 * minDim);
}
if (userConfig.ui.titleTextSize) {
userConfig.ui.titleTextSize = parseInt(userConfig.ui.titleTextSize, 10);
} else {
userConfig.ui.titleTextSize = Math.round(0.015 * minDim);
}
if (userConfig.ui.pointerSize) {
userConfig.ui.pointerSize = parseInt(userConfig.ui.pointerSize, 10);
} else {
userConfig.ui.pointerSize = Math.round(0.08 * minDim);
}
if (userConfig.ui.minWindowWidth) {
userConfig.ui.minWindowWidth = parseInt(userConfig.ui.minWindowWidth, 10);
} else {
userConfig.ui.minWindowWidth = Math.round(0.08 * minDim); // 8%
}
if (userConfig.ui.minWindowHeight) {
userConfig.ui.minWindowHeight = parseInt(userConfig.ui.minWindowHeight, 10);
} else {
userConfig.ui.minWindowHeight = Math.round(0.08 * minDim); // 8%
}
if (userConfig.ui.maxWindowWidth) {
userConfig.ui.maxWindowWidth = parseInt(userConfig.ui.maxWindowWidth, 10);
} else {
userConfig.ui.maxWindowWidth = Math.round(1.2 * maxDim); // 120%
}
if (userConfig.ui.maxWindowHeight) {
userConfig.ui.maxWindowHeight = parseInt(userConfig.ui.maxWindowHeight, 10);
} else {
userConfig.ui.maxWindowHeight = Math.round(1.2 * maxDim); // 120%
}
// Voice defaults
if (userConfig.voice_commands === undefined) {
userConfig.voice_commands = {
enabled: true,
log: false,
// The space matters for parsing
system_name: "sage "
};
}
// Voice command are enabled by default
if (userConfig.voice_commands.enabled === undefined) {
userConfig.voice_commands.enabled = true;
} else {
// Test for a true value: true, on, yes, 1, ...
if (sageutils.isTrue(userConfig.voice_commands.enabled)) {
userConfig.voice_commands.enabled = true;
} else {
userConfig.voice_commands.enabled = false;
}
}
// Voice logging is off by default
if (userConfig.voice_commands.log === undefined) {
userConfig.voice_commands.log = false;
} else {
// Test for a true value: true, on, yes, 1, ...
if (sageutils.isTrue(userConfig.voice_commands.log)) {
userConfig.voice_commands.log = true;
} else {
userConfig.voice_commands.log = false;
}
}
// What the system is called, for when it passively listens
if (userConfig.voice_commands.system_name === undefined) {
// Default is "sage" the " " is important for parsing purposes
userConfig.voice_commands.system_name = "sage ";
} else {
if (typeof userConfig.voice_commands.system_name === "string") {
userConfig.voice_commands.system_name = userConfig.voice_commands.system_name.trim();
if (userConfig.voice_commands.system_name.length === 0) {
userConfig.voice_commands.system_name = "sage ";
} else {
// The " " is important for parsing purposes
userConfig.voice_commands.system_name += " ";
}
} else {
userConfig.voice_commands.system_name = "sage ";
}
}
// Check the borders settings (for hidding the borders)
if (userConfig.dimensions === undefined) {
userConfig.dimensions = {};
}
// Overlapping tile dimension in pixels to allow edge blending
// tile_overlap = { horizontal: 20, vertical: 20}
// code provided by Larse Bilke
// larsbilke83@gmail.com
if (userConfig.dimensions.tile_overlap === undefined) {
userConfig.dimensions.tile_overlap = {
horizontal: 0,
vertical: 0
};
} else {
// Check the values
var hoverlap = parseInt(userConfig.dimensions.tile_overlap.horizontal, 10);
var voverlap = parseInt(userConfig.dimensions.tile_overlap.vertical, 10);
// If negative values, converted to positives
if (hoverlap < 0) {
hoverlap *= -1;
}
if (voverlap < 0) {
voverlap *= -1;
}
// Set the final values back into the configuration
userConfig.dimensions.tile_overlap = {
horizontal: hoverlap,
vertical: voverlap
};
}
// Tile config Basic mode
var aspectRatioConfig = userConfig.dimensions.aspect_ratio;
var aspectRatio = 1.7778; // 16:9
var userDefinedAspectRatio = false;
if (aspectRatioConfig !== undefined) {
var ratioParsed = aspectRatioConfig.split(":");
aspectRatio = (parseFloat(ratioParsed[0]) / parseFloat(ratioParsed[1])) || aspectRatio;
userDefinedAspectRatio = true;
sageutils.log("UI", "User defined aspect ratio:", aspectRatio);
}
var tileHeight = 0.0;
if (userConfig.dimensions.tile_diagonal_inches !== undefined) {
var tile_diagonal_meters = userConfig.dimensions.tile_diagonal_inches * 0.0254;
tileHeight = tile_diagonal_meters * aspectRatio;
}
if (userConfig.dimensions.tile_height) {
tileHeight = parseFloat(userConfig.dimensions.tile_height) || 0.0;
}
// calculate pixel density (ppm) based on width
var pixelsPerMeter = userConfig.resolution.height / tileHeight;
if (userDefinedAspectRatio == false) {
aspectRatio = userConfig.resolution.width / userConfig.resolution.height;
sageutils.log("UI", "Resolution defined aspect ratio:", aspectRatio.toFixed(2));
}
// Check the display border settings
if (userConfig.dimensions.tile_borders === undefined) {
// set default values to 0
// first for pixel sizes
userConfig.resolution.borders = { left: 0, right: 0, bottom: 0, top: 0};
// then for dimensions
userConfig.dimensions.tile_borders = { left: 0.0, right: 0.0, bottom: 0.0, top: 0.0};
} else {
var borderLeft, borderRight, borderBottom, borderTop;
// make sure the values are valid floats
borderLeft = parseFloat(userConfig.dimensions.tile_borders.left) || 0.0;
borderRight = parseFloat(userConfig.dimensions.tile_borders.right) || 0.0;
borderBottom = parseFloat(userConfig.dimensions.tile_borders.bottom) || 0.0;
borderTop = parseFloat(userConfig.dimensions.tile_borders.top) || 0.0;
// calculate values in pixel now
userConfig.resolution.borders = {};
userConfig.resolution.borders.left = Math.round(pixelsPerMeter * borderLeft) || 0;
userConfig.resolution.borders.right = Math.round(pixelsPerMeter * borderRight) || 0;
userConfig.resolution.borders.bottom = Math.round(pixelsPerMeter * borderBottom) || 0;
userConfig.resolution.borders.top = Math.round(pixelsPerMeter * borderTop) || 0;
}
// calculate the widget control size based on dimensions and user distance
if (userConfig.ui.auto_scale_ui && tileHeight !== undefined) {
var objectHeightMeters = 27 / pixelsPerMeter;
var minimumWidgetControlSize = 20; // Min button size for text readability (also for touch wall)
var perceptualScalingFactor = 0.0213;
var oldDefaultWidgetScale = Math.round(0.020 * minDim);
var userDist = userConfig.dimensions.viewing_distance;
var calcuatedWidgetControlSize = userDist * (perceptualScalingFactor * (userDist / objectHeightMeters));
var targetVisualAcuity = 0.5; // degrees of arc
calcuatedWidgetControlSize = Math.tan((targetVisualAcuity * Math.PI / 180.0) / 2) * 2 * userDist * pixelsPerMeter;
if (calcuatedWidgetControlSize < minimumWidgetControlSize) {
calcuatedWidgetControlSize = minimumWidgetControlSize;
sageutils.log("UI", "widgetControlSize (min):", calcuatedWidgetControlSize);
} else if (calcuatedWidgetControlSize > oldDefaultWidgetScale) {
calcuatedWidgetControlSize = oldDefaultWidgetScale * 2;
sageutils.log("UI", "widgetControlSize (max):", calcuatedWidgetControlSize);
} else {
sageutils.log("UI", "widgetControlSize:", calcuatedWidgetControlSize);
}
// sageutils.log("UI", "pixelsPerMeter:", pixelsPerMeter);
userConfig.ui.widgetControlSize = calcuatedWidgetControlSize;
}
// Automatically populate the displays entry if undefined. Adds left to right, starting from the top.
if (userConfig.displays === undefined || userConfig.displays.length == 0) {
userConfig.displays = [];
for (var r = 0; r < userConfig.layout.rows; r++) {
for (var c = 0; c < userConfig.layout.columns; c++) {
userConfig.displays.push({row: r, column: c});
}
}
}
// Check the width and height of each display (in tile count)
// by default, a display covers one tile
for (var d = 0; d < userConfig.displays.length; d++) {
userConfig.displays[d].width = parseInt(userConfig.displays[d].width) || 1;
userConfig.displays[d].height = parseInt(userConfig.displays[d].height) || 1;
}
// legacy support for config port names
var http_port, https_port;
if (userConfig.secure_port === undefined) {
http_port = userConfig.index_port;
https_port = userConfig.port;
delete userConfig.index_port;
} else {
http_port = userConfig.port;
https_port = userConfig.secure_port;
}
var rproxy_port, rproxys_port;
if (userConfig.rproxy_secure_port === undefined) {
rproxy_port = userConfig.rproxy_index_port;
rproxys_port = userConfig.rproxy_port;
delete userConfig.rproxy_index_port;
} else {
rproxy_port = userConfig.rproxy_port;
rproxys_port = userConfig.rproxy_secure_port;
}
// Set default values if missing
if (https_port === undefined) {
userConfig.secure_port = 443;
} else {
userConfig.secure_port = parseInt(https_port, 10); // to make sure it's a number
}
if (http_port === undefined) {
userConfig.port = 80;
} else {
userConfig.port = parseInt(http_port, 10);
}
userConfig.rproxy_port = parseInt(rproxy_port, 10) || undefined;
userConfig.rproxy_secure_port = parseInt(rproxys_port, 10) || undefined;
// Set the display clip value if missing (true by default)
if (userConfig.background.clip !== undefined) {
userConfig.background.clip = sageutils.isTrue(userConfig.background.clip);
} else {
userConfig.background.clip = true;
}
// Registration to EVL's server (sage.evl.uic.edu), true by default
if (userConfig.register_site === undefined) {
userConfig.register_site = true;
} else {
// test for a true value: true, on, yes, 1, ...
if (sageutils.isTrue(userConfig.register_site)) {
userConfig.register_site = true;
} else {
userConfig.register_site = false;
}
}
return userConfig;
}
var getUniqueAppId = function(param) {
// reset the counter
if (param && param === -1) {
getUniqueAppId.count = 0;
return;
}
var id = "app_" + getUniqueAppId.count.toString();
getUniqueAppId.count++;
return id;
};
getUniqueAppId.count = 0;
var getNewUserId = (function() {
var count = 0;
return function() {
var id = "usr_" + count.toString();
count++;
return id;
};
}());
function getUniqueDataSharingId(remoteHost, remotePort, caller) {
var id;
if (caller === true) {
id = config.host + ":" + config.secure_port + "+" + remoteHost + ":" + remotePort;
} else {
id = remoteHost + ":" + remotePort + "+" + config.host + ":" + config.secure_port;
}
return "portal_" + id;
}
function getUniqueSharedAppId(portalId) {
return "app_" + remoteSharingSessions[portalId].appCount + "_" + portalId;
}
function getSavedFilesList() {
// Get the sessions
var savedSessions = listSessions();
savedSessions.sort(sageutils.compareFilename);
// Get everything from the asset manager
var list = assets.listAssets();
// add the sessions
list.sessions = savedSessions;
return list;
}
function setupDisplayBackground() {
var tmpImg, imgExt;
// background image
if (config.background.image !== undefined && config.background.image.url !== undefined) {
var bg_file = path.join(publicDirectory, config.background.image.url);
if (config.background.image.style === "fit") {
exiftool.file(bg_file, function(err1, data) {
if (err1) {
console.log("Error processing background image:", bg_file, err1);
console.log(" ");
process.exit(1);
}
var bg_info = data;
if (bg_info.ImageWidth === config.totalWidth && bg_info.ImageHeight === config.totalHeight) {
sliceBackgroundImage(bg_file, bg_file);
} else {
tmpImg = path.join(publicDirectory, "images", "background", "tmp_background.png");
var out_res = config.totalWidth.toString() + "x" + config.totalHeight.toString();
imageMagick(bg_file).noProfile().command("convert").in("-gravity", "center")
.in("-background", "rgba(0,0,0,0)")
.in("-extent", out_res).write(tmpImg, function(err2) {
if (err2) {
throw err2;
}
sliceBackgroundImage(tmpImg, bg_file);
});
}
});
} else if (config.background.image.style === "tile") {
// do nothing
} else {
config.background.image.style = "stretch";
imgExt = path.extname(bg_file);
tmpImg = path.join(publicDirectory, "images", "background", "tmp_background" + imgExt);
imageMagick(bg_file).resize(config.totalWidth, config.totalHeight, "!").write(tmpImg, function(err) {
if (err) {
throw err;
}
sliceBackgroundImage(tmpImg, bg_file);
});
}
}
}
function sliceBackgroundImage(fileName, outputBaseName) {
for (var i = 0; i < config.displays.length; i++) {
var x = config.displays[i].column * config.resolution.width;
var y = config.displays[i].row * config.resolution.height;
var output_dir = path.dirname(outputBaseName);
var input_ext = path.extname(outputBaseName);
var output_ext = path.extname(fileName);
var output_base = path.basename(outputBaseName, input_ext);
var output = path.join(output_dir, output_base + "_" + i.toString() + output_ext);
imageMagick(fileName).crop(
config.resolution.width * config.displays[i].width,
config.resolution.height * config.displays[i].height, x, y
).write(output, function(err) {
if (err) {
console.log("error slicing image", err); // throw err;
}
});
}
}
function setupHttpsOptions() {
// build a list of certs to support multi-homed computers
var certs = {};
// file caching for the main key of the server
var server_key = null;
var server_crt = null;
var server_ca = [];
// add the default cert from the hostname specified in the config file
try {
// first try the filename based on the hostname-server.key
if (sageutils.fileExists(path.join("keys", config.host + "-server.key"))) {
// Load the certificate files
sageutils.log("Certificate", "Loading certificate", config.host + "-server.key");
server_key = fs.readFileSync(path.join("keys", config.host + "-server.key"));
server_crt = fs.readFileSync(path.join("keys", config.host + "-server.crt"));
server_ca = sageutils.loadCABundle(path.join("keys", config.host + "-ca.crt"));
// Build the crypto
certs[config.host] = sageutils.secureContext(server_key, server_crt, server_ca);
} else {
// remove the hostname from the FQDN and search for wildcard certificate
// syntax: _.rest.com.key or _.rest.bigger.com.key
var domain = '_.' + config.host.split('.').slice(1).join('.');
sageutils.log("Certificate", "Loading domain certificate", domain + ".key");
server_key = fs.readFileSync(path.join("keys", domain + ".key"));
server_crt = fs.readFileSync(path.join("keys", domain + ".crt"));
server_ca = sageutils.loadCABundle(path.join("keys", domain + "-ca.crt"));
certs[config.host] = sageutils.secureContext(server_key, server_crt, server_ca);
}
} catch (e) {
console.log("\n----------");
console.log("Cannot open certificate for default host:");
console.log(" \"" + config.host + "\" needs file: " + e.path);
console.log(" --> Please generate the appropriate certificate in the 'keys' folder");
console.log("----------\n\n");
process.exit(1);
}
for (var h in config.alternate_hosts) {
try {
var alth = config.alternate_hosts[h];
certs[ alth ] = sageutils.secureContext(
fs.readFileSync(path.join("keys", alth + "-server.key")),
fs.readFileSync(path.join("keys", alth + "-server.crt")),
sageutils.loadCABundle(path.join("keys", alth + "-ca.crt"))
);
} catch (e) {
console.log("\n----------");
console.log("Cannot open certificate for the alternate host: ", config.alternate_hosts[h]);
console.log(" needs file: \"" + e.path + "\"");
console.log(" --> Please generate the appropriate certificates in the 'keys' folder");
console.log(" Ignoring alternate host: ", config.alternate_hosts[h]);
console.log("----------\n");
}
}
var httpsOptions;
if (sageutils.nodeVersion === 10) {
httpsOptions = {
// server default keys
key: server_key,
cert: server_crt,
ca: server_ca,
// If true the server will request a certificate from clients that connect and attempt to verify that certificate
requestCert: false,
rejectUnauthorized: false,
// callback to handle multi-homed machines
SNICallback: function(servername) {
if (!certs.hasOwnProperty(servername)) {
sageutils.log("SNI", "Unknown host, cannot find a certificate for ", servername);
return null;
}
return certs[servername];
}
};
} else {
httpsOptions = {
// server default keys
key: server_key,
cert: server_crt,
ca: server_ca,
// If true the server will request a certificate from clients that connect and attempt to verify that certificate
requestCert: false,
rejectUnauthorized: false,
honorCipherOrder: true,
// callback to handle multi-homed machines
SNICallback: function(servername, cb) {
if (certs.hasOwnProperty(servername)) {
cb(null, certs[servername]);
} else {
sageutils.log("SNI", "Unknown host, cannot find a certificate for ", servername);
cb("SNI Unknown host", null);
}
}
};
// The SSL method to use, otherwise undefined
if (config.security && config.security.secureProtocol) {
// Possible values are defined in the constant SSL_METHODS of OpenSSL
// Only enable TLS 1.2 for instance
// SSLv3_method,
// TLSv1_method, TLSv1_1_method, TLSv1_2_method
// DTLS_method, DTLSv1_method, DTLSv1_2_method
httpsOptions.secureProtocol = config.security.secureProtocol;
sageutils.log("HTTPS", "securing with protocol:", httpsOptions.secureProtocol);
}
}
return httpsOptions;
}
function sendConfig(req, res) {
var header = HttpServer.prototype.buildHeader();
// Set type
header["Content-Type"] = "application/json";
// Allow CORS on the /config route
if (req.headers.origin !== undefined) {
header['Access-Control-Allow-Origin' ] = req.headers.origin;
header['Access-Control-Allow-Methods'] = "GET";
header['Access-Control-Allow-Headers'] = "X-Requested-With, X-HTTP-Method-Override, Content-Type, Accept";
header['Access-Control-Allow-Credentials'] = true;
}
res.writeHead(200, header);
// Adding the calculated version into the data structure
config.version = SAGE2_version;
res.write(JSON.stringify(config));
res.end();
}
function uploadForm(req, res) {
var form = new formidable.IncomingForm();
// Drop position
var position = [0, 0];
// Open or not the file after upload
var openAfter = true;
// User information
var ptrName = "";
var ptrColor = "";
// Limits the amount of memory all fields together (except files) can allocate in bytes.
// set to 4MB.
form.maxFieldsSize = 4 * 1024 * 1024;
form.type = 'multipart';
form.multiples = true;
form.on('fileBegin', function(name, file) {
sageutils.log("Upload", file.name, file.type);
});
form.on('error', function(err) {
sageutils.log("Upload", 'Request aborted');
try {
// Removing the temporary file
fs.unlinkSync(this.openedFiles[0].path);
} catch (err) {
sageutils.log("Upload", ' error removing the temporary file');
}
});
form.on('field', function(field, value) {
// Keep user information
if (field === 'SAGE2_ptrName') {
ptrName = value;
sageutils.log("Upload", "by", ptrName);
}
if (field === 'SAGE2_ptrColor') {
ptrColor = value;
sageutils.log("Upload", "color", ptrColor);
}
// convert value [0 to 1] to wall coordinate from drop location
if (field === 'dropX') {
position[0] = parseInt(parseFloat(value) * config.totalWidth, 10);
}
if (field === 'dropY') {
position[1] = parseInt(parseFloat(value) * config.totalHeight, 10);
}
// initial application window position
if (field === 'width') {
position[2] = parseInt(parseFloat(value) * config.totalWidth, 10);
}
if (field === 'height') {
position[3] = parseInt(parseFloat(value) * config.totalHeight, 10);
}
// open or not the file after upload
if (field === 'open') {
openAfter = (value === "true");
}
});
form.parse(req, function(err, fields, files) {
var header = HttpServer.prototype.buildHeader();
if (err) {
header["Content-Type"] = "text/plain";
res.writeHead(500, header);
res.write(err + "\n\n");
res.end();
return;
}
// build the reply to the upload
header["Content-Type"] = "application/json";
res.writeHead(200, header);
// For webix uploader: status: server
fields.done = true;
// Get the file (only one even if multiple drops, it comes one by one)
var file = files[ Object.keys(files)[0] ];
var app = registry.getDefaultApp(file.name);
if (app === undefined || app === "") {
fields.good = false;
} else {
fields.good = true;
}
// Send the reply
res.end(JSON.stringify({status: 'server',
fields: fields, files: files}));
});
form.on('end', function() {
// saves files in appropriate directory and broadcasts the items to the displays
manageUploadedFiles(this.openedFiles, position, ptrName, ptrColor, openAfter);
});
}
function manageUploadedFiles(files, position, ptrName, ptrColor, openAfter) {
var fileKeys = Object.keys(files);
fileKeys.forEach(function(key) {
var file = files[key];
appLoader.manageAndLoadUploadedFile(file, function(appInstance, videohandle) {
if (appInstance === null) {
sageutils.log("Upload", 'unrecognized file type:', file.name, file.type);
return;
}
// Add user information into exif data
assets.addTag(appInstance.file, "SAGE2user", ptrName);
assets.addTag(appInstance.file, "SAGE2color", ptrColor);
// contains a flag to open the file or not
if (openAfter) {
// Use the size from the drop information
if (position[2] && position[2] !== 0) {
appInstance.width = parseFloat(position[2]);
// If no height provided, calculate it from the aspect ratio
if (position[3] === undefined && appInstance.aspect) {
appInstance.height = appInstance.width / appInstance.aspect;
}
}
if (position[3] && position[3] !== 0) {
appInstance.height = parseFloat(position[3]);
}
// Use the position from the drop information
if (position[0] !== 0 || position[1] !== 0) {
appInstance.left = position[0] - appInstance.width / 2;
if (appInstance.left < 0) {
appInstance.left = 0;
}
appInstance.top = position[1] - appInstance.height / 2;
if (appInstance.top < 0) {
appInstance.top = 0;
}
}
appInstance.id = getUniqueAppId();
if (appInstance.animation) {
var i;
SAGE2Items.renderSync[appInstance.id] = {clients: {}, date: Date.now()};
for (i = 0; i < clients.length; i++) {
if (clients[i].clientType === "display") {
SAGE2Items.renderSync[appInstance.id].clients[clients[i].id] = {
wsio: clients[i], readyForNextFrame: false, blocklist: []
};
}
}
}
handleNewApplication(appInstance, videohandle);
}
// send the update file list
broadcast('storedFileList', getSavedFilesList());
});
});
}
// ************** Remote Site Collaboration *****************
function initalizeRemoteSites() {
if (config.remote_sites) {
remoteSites = new Array(config.remote_sites.length);
config.remote_sites.forEach(function(element, index, array) {
// if we have a valid definition of a remote site (host, port and name)
if (element.host && element.port && element.name) {
var protocol = (element.secure === true) ? "wss" : "ws";
var wsURL = protocol + "://" + element.host + ":" + element.port.toString();
var rGeom = {};
rGeom.w = Math.min((0.5 * config.totalWidth) / remoteSites.length, config.ui.titleBarHeight * 6)
- (0.16 * config.ui.titleBarHeight);
rGeom.h = 0.84 * config.ui.titleBarHeight;
rGeom.x = (0.5 * config.totalWidth) + ((rGeom.w + (0.16 * config.ui.titleBarHeight))
* (index - (remoteSites.length / 2))) + (0.08 * config.ui.titleBarHeight);
rGeom.y = 0.08 * config.ui.titleBarHeight;
// Build the object
remoteSites[index] = {
name: element.name,
wsio: null,
connected: "off",
geometry: rGeom,
index: index
};
// Create a websocket connection to the site
remoteSites[index].wsio = createRemoteConnection(wsURL, element, index);
// Add the gemeotry for the button
interactMgr.addGeometry("remote_" + index, "staticUI", "rectangle", rGeom, true, index, remoteSites[index]);
// attempt to connect every 15 seconds, if connection failed
setInterval(function() {
if (remoteSites[index].connected !== "on") {
var rem = createRemoteConnection(wsURL, element, index);
remoteSites[index].wsio = rem;
}
}, 15000);
} else {
// not a valid site definition, we ignore it
sageutils.log("Remote", chalk.bold.red('invalid host definition (ignored)'), element.name);
}
});
}
}
function manageRemoteConnection(remote, site, index) {
// Fix address
remote.updateRemoteAddress(site.host, site.port);
// Hope for the best
remoteSites[index].connected = "on";
// Check the password or session hash
if (site.password) {
// MD5 hash of the password
site.session = md5.getHash(site.password);
}
var clientDescription = {
clientType: "remoteServer",
host: config.host,
port: config.secure_port,
session: site.session,
// port: config.port,
requests: {
config: false,
version: false,
time: false,
console: false
}
};
remote.clientType = "remoteServer";
remote.onclose(function() {
sageutils.log("Remote", chalk.cyan(config.remote_sites[index].name), "offline");
// it was locked, keep the state locked
if (remoteSites[index].connected !== "locked") {
remoteSites[index].connected = "off";
var delete_site = {name: remoteSites[index].name, connected: remoteSites[index].connected};
broadcast('connectedToRemoteSite', delete_site);
}
removeElement(clients, remote);
try {
// For debugging connections and slow down. Remote site just disconnected, update connection count.
sharedServerData.updateInformationAboutConnections(clients, sagePointers);
} catch (e) {
sageutils.log("Connections", "Error with updating client data");
sageutils.log("Connections", e);
}
});
remote.on('addClient', wsAddClient);
remote.on('addNewElementFromRemoteServer', wsAddNewElementFromRemoteServer);
remote.on('addNewSharedElementFromRemoteServer', wsAddNewSharedElementFromRemoteServer);
remote.on('requestNextRemoteFrame', wsRequestNextRemoteFrame);
remote.on('updateRemoteMediaStreamFrame', wsUpdateRemoteMediaStreamFrame);
remote.on('stopMediaStream', wsStopMediaStream);
remote.on('requestNextRemoteBlockFrame', wsRequestNextRemoteBlockFrame);
remote.on('updateRemoteMediaBlockStreamFrame', wsUpdateRemoteMediaBlockStreamFrame);
remote.on('stopMediaBlockStream', wsStopMediaBlockStream);
remote.on('requestDataSharingSession', wsRequestDataSharingSession);
remote.on('cancelDataSharingSession', wsCancelDataSharingSession);
remote.on('acceptDataSharingSession', wsAcceptDataSharingSession);
remote.on('rejectDataSharingSession', wsRejectDataSharingSession);
remote.on('createRemoteSagePointer', wsCreateRemoteSagePointer);
remote.on('startRemoteSagePointer', wsStartRemoteSagePointer);
remote.on('stopRemoteSagePointer', wsStopRemoteSagePointer);
remote.on('remoteSagePointerPosition', wsRemoteSagePointerPosition);
remote.on('remoteSagePointerToggleModes', wsRemoteSagePointerToggleModes);
remote.on('remoteSagePointerHoverCorner', wsRemoteSagePointerHoverCorner);
remote.on('addNewRemoteElementInDataSharingPortal', wsAddNewRemoteElementInDataSharingPortal);
remote.on('updateApplicationOrder', wsUpdateApplicationOrder);
remote.on('startApplicationMove', wsStartApplicationMove);
remote.on('startApplicationResize', wsStartApplicationResize);
remote.on('updateApplicationPosition', wsUpdateApplicationPosition);
remote.on('updateApplicationPositionAndSize', wsUpdateApplicationPositionAndSize);
remote.on('finishApplicationMove', wsFinishApplicationMove);
remote.on('finishApplicationResize', wsFinishApplicationResize);
remote.on('deleteApplication', wsDeleteApplication);
remote.on('updateApplicationState', wsUpdateApplicationState);
remote.on('updateApplicationStateOptions', wsUpdateApplicationStateOptions);
remote.emit('addClient', clientDescription);
clients.push(remote);
remote.on('remoteConnection', function(remotesocket, data) {
if (data.status === "refused") {
sageutils.log("Remote", "Connection refused to", chalk.cyan(site.name), ": " + data.reason);
remoteSites[index].connected = "locked";
} else {
sageutils.log("Remote", "Connected to", chalk.cyan(site.name));
remoteSites[index].connected = "on";
}
var update_site = {name: remoteSites[index].name, connected: remoteSites[index].connected};
broadcast('connectedToRemoteSite', update_site);
});
}
function createRemoteConnection(wsURL, element, index) {
var remote = new WebsocketIO(wsURL, false, function() {
manageRemoteConnection(remote, element, index);
});
return remote;
}
// ************** System Time - Updated Every Minute *****************
var cDate = new Date();
setTimeout(function() {
var now;
setInterval(function() {
now = new Date();
broadcast('setSystemTime', {date: now.toJSON(), offset: now.getTimezoneOffset()});
}, 60000);
now = new Date();
broadcast('setSystemTime', {date: now.toJSON(), offset: now.getTimezoneOffset()});
}, (61 - cDate.getSeconds()) * 1000);
// ***************************************************************************************
// Place callback for success in the 'listen' call for HTTPS
sage2ServerS.on('listening', function(e) {
// Success
sageutils.log("SAGE2", chalk.bold("Serving Securely:"));
sageutils.log("SAGE2", "- Web UI:\t " + chalk.cyan.bold.underline("https://" +
config.host + ":" + config.secure_port));
sageutils.log("SAGE2", "- Web console:\t " + chalk.cyan.bold.underline("https://" + config.host +
":" + config.secure_port + "/admin/console.html"));
});
// Place callback for errors in the 'listen' call for HTTP
sage2Server.on('error', function(e) {
if (e.code === 'EACCES') {
sageutils.log("HTTP_Server", "You are not allowed to use the port: ", config.port);
sageutils.log("HTTP_Server", " use a different port or get authorization (sudo, setcap, ...)");
process.exit(1);
} else if (e.code === 'EADDRINUSE') {
sageutils.log("HTTP_Server", "The port is already in use by another process:", config.port);
sageutils.log("HTTP_Server", " use a different port or stop the offending process");
process.exit(1);
} else {
sageutils.log("HTTP_Server", "Error in the listen call: ", e.code);
process.exit(1);
}
});
// Place callback for success in the 'listen' call for HTTP
sage2Server.on('listening', function(e) {
// Success
var ui_url = chalk.cyan.bold.underline("http://" + config.host + ":" + config.port);
var dp_url = chalk.cyan.bold.underline("http://" + config.host + ":" + config.port +
"/display.html?clientID=0");
var am_url = chalk.cyan.bold.underline("http://" + config.host + ":" + config.port +
"/audioManager.html");
if (global.__SESSION_ID) {
ui_url = chalk.cyan.bold.underline("http://" + config.host + ":" + config.port +
"/session.html?hash=" + global.__SESSION_ID);
dp_url = chalk.cyan.bold.underline("http://" + config.host + ":" + config.port +
"/session.html?page=display.html?clientID=0&hash=" + global.__SESSION_ID);
am_url = chalk.cyan.bold.underline("http://" + config.host + ":" + config.port +
"/session.html?page=audioManager.html&hash=" + global.__SESSION_ID);
}
sageutils.log("SAGE2", chalk.bold("Serving:"));
sageutils.log("SAGE2", "- Web UI:\t" + ui_url);
sageutils.log("SAGE2", "- Display 0:\t" + dp_url);
sageutils.log("SAGE2", "- Audio mgr:\t" + am_url);
});
// KILL intercept
process.on('SIGTERM', quitSAGE2);
// CTRL-C intercept
process.on('SIGINT', quitSAGE2);
// Start the HTTP server (listen for IPv4 addresses 0.0.0.0)
sage2Server.listen(config.port, "0.0.0.0");
// Start the HTTPS server (listen for IPv4 addresses 0.0.0.0)
sage2ServerS.listen(config.secure_port, "0.0.0.0");
// ***************************************************************************************
// Load session file if specified on the command line (-s)
if (program.session) {
setTimeout(function() {
// if -s specified without argument
if (program.session === true) {
loadSession();
} else {
// if argument specified
loadSession(program.session);
}
}, 1000);
}
function getSAGE2Path(getName) {
// pathname: result of the search
var pathname = null;
// walk through the list of folders
for (var f in mediaFolders) {
// Get the folder object
var folder = mediaFolders[f];
// Look for the folder url in the request
var pubdir = getName.split(folder.url);
if (pubdir.length === 2) {
// convert the URL into a path
var suburl = path.join('.', pubdir[1]);
pathname = url.resolve(folder.path, suburl);
pathname = decodeURIComponent(pathname);
break;
}
}
// if everything fails, look in the default public folder
if (!pathname) {
pathname = getName;
}
return pathname;
}
function processInputCommand(line) {
// split the command line at whitespace(s)
var command = line.trim().split(/[\s]+/);
switch (command[0]) {
case '': {
// ignore
break;
}
case 'help': {
console.log('help\t\tlist commands');
console.log('kill\t\tclose application: appid');
console.log('apps\t\tlist running applications');
console.log('clients\t\tlist connected clients');
console.log('streams\t\tlist media streams');
console.log('clear\t\tclose all running applications');
console.log('tile\t\tlayout all running applications');
console.log('fullscreen\tmaximize one application: appid');
console.log('save\t\tsave state of running applications into a session');
console.log('load\t\tload a session and restore applications');
console.log('open\t\topen a file: open file_url [0.5, 0.5]');
console.log('resize\t\tresize a window: appid width height');
console.log('moveby\t\tshift a window: appid dx dy');
console.log('moveto\t\tmove a window: appid x y');
console.log('assets\t\tlist the assets in the file library');
console.log('regenerate\tregenerates the assets');
console.log('hideui\t\thide/show/delay the user interface');
console.log('sessions\tlist the available sessions');
console.log('screenshot\ttake a screenshot of the wall');
console.log('update\t\trun a git update');
console.log('performance\tshow performance information');
console.log('perfsampling\tset performance metric sampling rate');
console.log('hardware\tget an summary of the hardware running the server');
console.log('update\t\trun a git update');
console.log('version\t\tprint SAGE2 version');
console.log('exit\t\tstop SAGE2');
break;
}
case 'version': {
sageutils.log("Version", 'base:', SAGE2_version.base, 'branch:', SAGE2_version.branch,
'commit:', SAGE2_version.commit, SAGE2_version.date);
break;
}
case 'update': {
if (SAGE2_version.branch.length > 0) {
sageutils.updateWithGIT(SAGE2_version.branch, function(error, success) {
if (error) {
sageutils.log('GIT', 'Update error -', error);
} else {
sageutils.log('Update success -', success);
}
});
} else {
sageutils.log("Update", "failed: not linked to any repository");
}
break;
}
case 'save': {
if (command[1] !== undefined) {
saveSession(command[1]);
} else {
saveSession();
}
break;
}
case 'load': {
if (command[1] !== undefined) {
loadSession(command[1]);
} else {
loadSession();
}
break;
}
case 'open': {
if (command[1] !== undefined) {
var pos = [0.0, 0.0];
var file = command[1];
if (command.length === 4) {
pos = [parseFloat(command[2]), parseFloat(command[3])];
}
var mt = assets.getMimeType(getSAGE2Path(file));
if (mt === "application/custom") {
wsLoadApplication({id: "127.0.0.1:42"}, {
application: file,
user: "127.0.0.1:42",
position: pos
});
} else {
wsLoadFileFromServer({id: "127.0.0.1:42"}, {
application: "something",
filename: file,
user: "127.0.0.1:42",
position: pos
});
}
} else {
sageutils.log("Command", "should be: open /user/file.pdf [0.5 0.5]");
}
break;
}
case 'sessions': {
printListSessions();
break;
}
case 'screenshot': {
// send messages to take screenshot (dummy arguments)
wsStartWallScreenshot();
break;
}
case 'moveby': {
// command: moveby appid dx dy (relative, in pixels)
if (command.length === 4) {
var dx = parseFloat(command[2]);
var dy = parseFloat(command[3]);
wsAppMoveBy(null, {id: command[1], dx: dx, dy: dy});
} else {
sageutils.log("Command", "should be: moveby app_0 10 10");
}
break;
}
case 'moveto': {
// command: moveti appid x y (absolute, in pixels)
if (command.length === 4) {
var xx = parseFloat(command[2]);
var yy = parseFloat(command[3]);
wsAppMoveTo(null, {id: command[1], x: xx, y: yy});
} else {
sageutils.log("Command", "should be: moveto app_0 100 100");
}
break;
}
case 'resize': {
var ww, hh;
// command: resize appid width height (force exact resize)
// command: resize appid width (keep aspect ratio)
if (command.length === 4) {
ww = parseFloat(command[2]);
hh = parseFloat(command[3]);
wsAppResize(null, {id: command[1], width: ww, height: hh, keepRatio: false});
sageutils.log("Command", "resizing exactly to", ww + "x" + hh);
} else if (command.length === 3) {
ww = parseFloat(command[2]);
hh = 0;
wsAppResize(null, {id: command[1], width: ww, height: hh, keepRatio: true});
} else {
sageutils.log("Command", "should be: resize app_0 800 600");
}
break;
}
case 'hideui': {
// if argument provided, used as auto_hide delay in second
// otherwise, it flips a switch
if (command[1] !== undefined) {
broadcast('hideui', {delay: parseInt(command[1], 10)});
} else {
broadcast('hideui', null);
}
break;
}
case 'close':
case 'delete':
case 'kill': {
if (command.length > 1 && typeof command[1] === "string") {
deleteApplication(command[1]);
}
break;
}
case 'fullscreen': {
if (command.length > 1 && typeof command[1] === "string") {
wsFullscreen(null, {id: command[1]});
} else {
sageutils.log("Command", "should be: fullscreen app_0");
}
break;
}
case 'clear': {
clearDisplay();
break;
}
case 'assets': {
assets.printAssets();
break;
}
case 'regenerate': {
assets.regenerateAssets();
break;
}
case 'tile': {
tileApplications();
break;
}
case 'clients': {
listClients();
break;
}
case 'apps': {
listApplications();
break;
}
case 'streams': {
listMediaStreams();
break;
}
case 'blockStreams': {
listMediaBlockStreams();
break;
}
case 'perfsampling':
if (command.length > 1) {
performanceManager.setSamplingInterval(command[1]);
} else {
sageutils.log("Command", "should be: perfsampling [slow|normal|often]");
}
break;
case 'performance':
performanceManager.printMetrics();
break;
case 'hardware':
performanceManager.printServerHardware();
break;
case 'exit':
case 'quit':
case 'bye': {
quitSAGE2();
break;
}
default: {
console.log('Say what? I might have heard `' + line.trim() + '`');
break;
}
}
}
// Command loop: reading input commands - SHOULD MOVE LATER: INSIDE CALLBACK AFTER SERVER IS LISTENING
if (program.interactive) {
// Create line reader for stdin and stdout
var shell = readline.createInterface({
input: process.stdin, output: process.stdout
});
// Set the prompt
shell.setPrompt("> ");
// Callback for each line
shell.on('line', function(line) {
processInputCommand(line);
shell.prompt();
}).on('close', function() {
// Saving stuff
quitSAGE2();
});
}
// ***************************************************************************************
function formatDateToYYYYMMDD_HHMMSS(date) {
var year = date.getFullYear();
var month = date.getMonth() + 1;
var day = date.getDate();
var hour = date.getHours();
var minute = date.getMinutes();
var second = date.getSeconds();
year = year.toString();
month = month >= 10 ? month.toString() : "0" + month.toString();
day = day >= 10 ? day.toString() : "0" + day.toString();
hour = hour >= 10 ? hour.toString() : "0" + hour.toString();
minute = minute >= 10 ? minute.toString() : "0" + minute.toString();
second = second >= 10 ? second.toString() : "0" + second.toString();
return year + "-" + month + "-" + day + "_" + hour + "-" + minute + "-" + second;
}
function quitSAGE2() {
if (config.register_site) {
// de-register with EVL's server
sageutils.deregisterSAGE2(config, function() {
userlist.save();
saveUserLog();
saveSession();
assets.saveAssets();
if (omicronRunning) {
omicronManager.disconnect();
}
process.exit(0);
});
} else {
userlist.save();
saveUserLog();
saveSession();
assets.saveAssets();
if (omicronRunning) {
omicronManager.disconnect();
}
process.exit(0);
}
}
function findRemoteSiteByConnection(wsio) {
var remoteIdx = -1;
for (var i = 0; i < config.remote_sites.length; i++) {
if (wsio.remoteAddress.address === config.remote_sites[i].host &&
wsio.remoteAddress.port === config.remote_sites[i].port) {
remoteIdx = i;
}
}
if (remoteIdx >= 0) {
return remoteSites[remoteIdx];
}
return null;
}
function hideControl(ctrl) {
if (ctrl.show === true) {
ctrl.show = false;
broadcast('hideControl', {id: ctrl.id, appId: ctrl.appId});
interactMgr.editVisibility(ctrl.id, "widgets", false);
}
}
function removeControlsForUser(uniqueID) {
var widgets = SAGE2Items.widgets.list;
for (var w in widgets) {
if (widgets.hasOwnProperty(w) && widgets[w].id.indexOf(uniqueID) > -1) {
interactMgr.removeGeometry(widgets[w].id, "widgets");
SAGE2Items.widgets.removeItem(widgets[w].id);
}
}
broadcast('removeControlsForUser', {user_id: uniqueID});
}
function showControl(ctrl, uniqueID, pointerX, pointerY) {
if (ctrl.show === false) {
ctrl.show = true;
interactMgr.editVisibility(ctrl.id, "widgets", true);
moveControlToPointer(ctrl, uniqueID, pointerX, pointerY);
broadcast('showControl', {
id: ctrl.id, appId: ctrl.appId,
user_color: sagePointers[uniqueID] ? sagePointers[uniqueID].color : null
});
}
}
function moveControlToPointer(ctrl, uniqueID, pointerX, pointerY) {
var dt = new Date();
var rightMargin = config.totalWidth - ctrl.width;
var bottomMargin = config.totalHeight - ctrl.height;
ctrl.left = (pointerX > rightMargin) ? rightMargin : pointerX - ctrl.height / 2;
ctrl.top = (pointerY > bottomMargin) ? bottomMargin : pointerY - ctrl.height / 2;
var radialGeometry = {
x: ctrl.left + (ctrl.height / 2),
y: ctrl.top + (ctrl.height / 2),
r: ctrl.height / 2
};
if (ctrl.hasSideBar === true) {
var shapeData = {
radial: {
type: "circle",
visible: true,
geometry: radialGeometry
},
sidebar: {
type: "rectangle",
visible: true,
geometry: {
x: ctrl.left + ctrl.height,
y: ctrl.top + (ctrl.height / 2) - (ctrl.barHeight / 2),
w: ctrl.width - ctrl.height, h: ctrl.barHeight
}
}
};
interactMgr.editComplexGeometry(ctrl.id, "widgets", shapeData);
} else {
interactMgr.editGeometry(ctrl.id, "widgets", "circle", radialGeometry);
}
var app = SAGE2Items.applications.list[ctrl.appId];
var appPos = (app === null) ? null : getAppPositionSize(app);
broadcast('setControlPosition', {date: dt, elemId: ctrl.id, elemLeft: ctrl.left, elemTop: ctrl.top,
elemHeight: ctrl.height, appData: appPos});
}
function initializeArray(size, val) {
var arr = new Array(size);
for (var i = 0; i < size; i++) {
arr[i] = val;
}
return arr;
}
function allNonBlank(arr) {
for (var i = 0; i < arr.length; i++) {
if (arr[i] === "") {
return false;
}
}
return true;
}
function allTrueDict(dict, property) {
var key;
for (key in dict) {
if (property === undefined && dict[key] !== true) {
return false;
}
if (property !== undefined && dict[key][property] !== true) {
return false;
}
}
return true;
}
function removeElement(list, elem) {
if (list.indexOf(elem) >= 0) {
moveElementToEnd(list, elem);
list.pop();
}
}
function moveElementToEnd(list, elem) {
var i;
var pos = list.indexOf(elem);
if (pos < 0) {
return;
}
for (i = pos; i < list.length - 1; i++) {
list[i] = list[i + 1];
}
list[list.length - 1] = elem;
}
function intToByteBuffer(aInt, bytes) {
var buf = new Buffer(bytes);
var byteVal;
var num = aInt;
for (var i = 0; i < bytes; i++) {
byteVal = num & 0xff;
buf[i] = byteVal;
num = (num - byteVal) / 256;
}
return buf;
}
function byteBufferToString(buf) {
var str = "";
var i = 0;
while (buf[i] !== 0 && i < buf.length) {
str += String.fromCharCode(buf[i]);
i++;
}
return str;
}
function addEventToUserLog(id, data) {
var key;
for (key in users) {
if (users[key].ip && users[key].ip === id) {
users[key].actions.push(data);
}
}
}
function getAppPositionSize(appInstance) {
return {
id: appInstance.id,
application: appInstance.application,
left: appInstance.left,
top: appInstance.top,
width: appInstance.width,
height: appInstance.height,
icon: appInstance.icon || null,
title: appInstance.title,
color: appInstance.color || null,
sticky: appInstance.sticky
};
}
// ************** Pointer Functions *****************
function createSagePointer(uniqueID, portal) {
// From addClient type == sageUI
sagePointers[uniqueID] = new Sagepointer(uniqueID + "_pointer");
sagePointers[uniqueID].portal = portal;
remoteInteraction[uniqueID] = new Interaction(config);
remoteInteraction[uniqueID].local = portal ? false : true;
broadcast('createSagePointer', sagePointers[uniqueID]);
}
function showPointer(uniqueID, data) {
if (sagePointers[uniqueID] === undefined) {
return;
}
sageutils.log("Pointer", chalk.green.bold("starting:"), chalk.underline.bold(uniqueID));
if (data.sourceType === undefined) {
data.sourceType = "Pointer";
}
sagePointers[uniqueID].start(data.label, data.color, data.sourceType);
broadcast('showSagePointer', sagePointers[uniqueID]);
}
function hidePointer(uniqueID) {
if (sagePointers[uniqueID] === undefined) {
return;
}
sageutils.log("Pointer", chalk.red.bold("stopping:"), chalk.underline.bold(uniqueID));
sagePointers[uniqueID].stop();
var prevInteractionItem = remoteInteraction[uniqueID].getPreviousInteractionItem();
if (prevInteractionItem !== null) {
showOrHideWidgetLinks({uniqueID: uniqueID, show: false, item: prevInteractionItem});
remoteInteraction[uniqueID].setPreviousInteractionItem(null);
}
broadcast('hideSagePointer', sagePointers[uniqueID]);
}
function globalToLocal(globalX, globalY, type, geometry) {
var local = {};
if (type === "circle") {
local.x = globalX - (geometry.x - geometry.r);
local.y = globalY - (geometry.y - geometry.r);
} else {
local.x = globalX - geometry.x;
local.y = globalY - geometry.y;
}
return local;
}
function pointerPress(uniqueID, pointerX, pointerY, data) {
if (sagePointers[uniqueID] === undefined) {
return;
}
// Middle click changes interaction mode
if (data.button === "middle") {
remoteInteraction[uniqueID].toggleModes();
broadcast('changeSagePointerMode', {id: sagePointers[uniqueID].id, mode: remoteInteraction[uniqueID].interactionMode});
}
var color = sagePointers[uniqueID] ? sagePointers[uniqueID].color : null;
// Whiteboard app
// If the user touches on the palette with drawing disabled, enable it
if ((!drawingManager.drawingMode) && drawingManager.touchInsidePalette(pointerX, pointerY)) {
// drawingManager.reEnableDrawingMode();
}
if (drawingManager.drawingMode) {
drawingManager.pointerEvent(
omicronManager.sageToOmicronEvent(uniqueID, pointerX, pointerY, data, 5, color),
uniqueID, pointerX, pointerY, 10, 10);
}
var obj = interactMgr.searchGeometry({x: pointerX, y: pointerY});
if (obj === null) {
pointerPressOnOpenSpace(uniqueID, pointerX, pointerY, data);
return;
}
// while cutting partition, can right click to cancel action
if (cuttingPartition[uniqueID] && data.button === "right") {
if (cuttingPartition[uniqueID].newPtn1) {
deletePartition(cuttingPartition[uniqueID].newPtn1.id);
}
if (cuttingPartition[uniqueID].newPtn2) {
deletePartition(cuttingPartition[uniqueID].newPtn2.id);
}
delete cuttingPartition[uniqueID];
}
// while dragging to create partition, can right click to cancel action
if (draggingPartition[uniqueID] && data.button === "right") {
deletePartition(draggingPartition[uniqueID].ptn.id);
delete draggingPartition[uniqueID];
}
var prevInteractionItem = remoteInteraction[uniqueID].getPreviousInteractionItem();
var localPt = globalToLocal(pointerX, pointerY, obj.type, obj.geometry);
switch (obj.layerId) {
case "staticUI": {
pointerPressOnStaticUI(uniqueID, pointerX, pointerY, data, obj, localPt);
break;
}
case "radialMenus": {
pointerPressOnRadialMenu(uniqueID, pointerX, pointerY, data, obj, localPt, color);
break;
}
case "widgets": {
if (prevInteractionItem === null) {
remoteInteraction[uniqueID].pressOnItem(obj);
showOrHideWidgetLinks({uniqueID: uniqueID, item: obj, user_color: color, show: true});
}
pointerPressOrReleaseOnWidget(uniqueID, pointerX, pointerY, data, obj, localPt, "press");
break;
}
case "applications": {
if (prevInteractionItem === null) {
remoteInteraction[uniqueID].pressOnItem(obj);
showOrHideWidgetLinks({uniqueID: uniqueID, item: obj, user_color: color, show: true});
}
pointerPressOnApplication(uniqueID, pointerX, pointerY, data, obj, localPt, null);
break;
}
case "partitions": {
pointerPressOnPartition(uniqueID, pointerX, pointerY, data, obj, localPt, null);
break;
}
case "portals": {
pointerPressOnDataSharingPortal(uniqueID, pointerX, pointerY, data, obj, localPt);
break;
}
}
}
function pointerPressOnOpenSpace(uniqueID, pointerX, pointerY, data) {
if (data.button === "right") {
// Right click opens the radial menu
createRadialMenu(uniqueID, pointerX, pointerY);
} else if (data.button === "left" && sagePointers[uniqueID].visible && remoteInteraction[uniqueID].CTRL) {
// CTRL with pointer open will begin to drag to create a new parititon
// start tracking size to create new partition
draggingPartition[uniqueID] = {};
draggingPartition[uniqueID].ptn = createPartition({left: pointerX, top: pointerY, width: 0, height: 0},
sagePointers[uniqueID].color);
draggingPartition[uniqueID].start = {x: pointerX, y: pointerY};
}
}
function pointerPressOnStaticUI(uniqueID, pointerX, pointerY, data, obj, localPt) {
// If the remote site is active (green button)
// also disable action through the web ui (visible pointer)
if (obj.data.connected === "on" && sagePointers[uniqueID].visible) {
// Validate the remote address
var remoteSite = findRemoteSiteByConnection(obj.data.wsio);
// Build the UI URL
var viewURL = 'https://' + remoteSite.wsio.remoteAddress.address + ':'
+ remoteSite.wsio.remoteAddress.port;
// pass the password or hash to the URL
if (config.remote_sites[remoteSite.index].password) {
viewURL += '/session.html?page=index.html?viewonly=true&session=' +
config.remote_sites[remoteSite.index].password;
} else if (config.remote_sites[remoteSite.index].hash) {
viewURL += '/session.html?page=index.html?viewonly=true&hash=' +
config.remote_sites[remoteSite.index].hash;
} else {
// no password
viewURL += '/index.html?viewonly=true';
}
// Create the webview to the remote UI
wsLoadApplication({id: uniqueID}, {
application: "/uploads/apps/Webview",
user: uniqueID,
// pass the url in the data object
data: {
id: uniqueID,
url: viewURL
},
position: [pointerX, config.ui.titleBarHeight + 10],
dimensions: [400, 120]
});
}
// don't allow data-pushing
/*
switch (obj.id) {
case "dataSharingRequestDialog": {
break;
}
case "dataSharingWaitDialog": {
break;
}
case "acceptDataSharingRequest": {
console.log("Accepting Data-Sharing Request");
broadcast('closeRequestDataSharingDialog', null);
var sharingMin = Math.min(remoteSharingRequestDialog.config.totalWidth,
remoteSharingRequestDialog.config.totalHeight - remoteSharingRequestDialog.config.ui.titleBarHeight);
var myMin = Math.min(config.totalWidth, config.totalHeight - config.ui.titleBarHeight);
var sharingSize = parseInt(0.45 * (sharingMin + myMin), 10);
var sharingScale = (0.9 * myMin) / sharingSize;
var sharingTitleBarHeight = (remoteSharingRequestDialog.config.ui.titleBarHeight + config.ui.titleBarHeight) / 2;
remoteSharingRequestDialog.wsio.emit('acceptDataSharingSession',
{width: sharingSize, height: sharingSize, titleBarHeight: sharingTitleBarHeight, date: Date.now()});
createNewDataSharingSession(remoteSharingRequestDialog.config.name,
remoteSharingRequestDialog.config.host, remoteSharingRequestDialog.config.port,
remoteSharingRequestDialog.wsio, null, sharingSize, sharingSize, sharingScale,
sharingTitleBarHeight, false);
remoteSharingRequestDialog = null;
showRequestDialog(false);
break;
}
case "rejectDataSharingRequest": {
console.log("Rejecting Data-Sharing Request");
broadcast('closeRequestDataSharingDialog', null);
remoteSharingRequestDialog.wsio.emit('rejectDataSharingSession', null);
remoteSharingRequestDialog = null;
showRequestDialog(false);
break;
}
case "cancelDataSharingRequest": {
console.log("Canceling Data-Sharing Request");
broadcast('closeDataSharingWaitDialog', null);
remoteSharingWaitDialog.wsio.emit('cancelDataSharingSession', null);
remoteSharingWaitDialog = null;
showWaitDialog(false);
break;
}
default: {
// remote site icon
requestNewDataSharingSession(obj.data);
}
}
*/
}
function createNewDataSharingSession(remoteName, remoteHost, remotePort, remoteWSIO, remoteTime,
sharingWidth, sharingHeight, sharingScale, sharingTitleBarHeight, caller) {
var zIndex = SAGE2Items.applications.numItems + SAGE2Items.portals.numItems;
var dataSession = {
id: getUniqueDataSharingId(remoteHost, remotePort, caller),
name: remoteName,
host: remoteHost,
port: remotePort,
left: config.ui.titleBarHeight,
top: 1.5 * config.ui.titleBarHeight,
width: sharingWidth * sharingScale,
height: sharingHeight * sharingScale,
previous_left: config.ui.titleBarHeight,
previous_top: 1.5 * config.ui.titleBarHeight,
previous_width: sharingWidth * sharingScale,
previous_height: sharingHeight * sharingScale,
natural_width: sharingWidth,
natural_height: sharingHeight,
aspect: sharingWidth / sharingHeight,
scale: sharingScale,
titleBarHeight: sharingTitleBarHeight,
zIndex: zIndex
};
console.log("New Data Sharing Session: " + dataSession.id);
var geometry = {
x: dataSession.left,
y: dataSession.top,
w: dataSession.width,
h: dataSession.height + config.ui.titleBarHeight
};
var cornerSize = 0.2 * Math.min(geometry.w, geometry.h);
var oneButton = Math.round(config.ui.titleBarHeight) * (300 / 235);
var buttonsPad = 0.1 * oneButton;
var startButtons = geometry.w - Math.round(2 * oneButton + buttonsPad);
/*
var buttonsWidth = (config.ui.titleBarHeight-4) * (324.0/111.0);
var buttonsPad = (config.ui.titleBarHeight-4) * ( 10.0/111.0);
var oneButton = buttonsWidth / 2; // two buttons
var startButtons = geometry.w - buttonsWidth;
*/
interactMgr.addGeometry(dataSession.id, "portals", "rectangle", geometry, true, zIndex, dataSession);
SAGE2Items.portals.addItem(dataSession);
SAGE2Items.portals.addButtonToItem(dataSession.id, "titleBar", "rectangle",
{x: 0, y: 0, w: geometry.w, h: config.ui.titleBarHeight}, 0);
SAGE2Items.portals.addButtonToItem(dataSession.id, "fullscreenButton", "rectangle",
{x: startButtons + buttonsPad, y: 0, w: oneButton, h: config.ui.titleBarHeight}, 1);
SAGE2Items.portals.addButtonToItem(dataSession.id, "closeButton", "rectangle",
{x: startButtons + buttonsPad + oneButton, y: 0, w: oneButton, h: config.ui.titleBarHeight}, 1);
SAGE2Items.portals.addButtonToItem(dataSession.id, "dragCorner", "rectangle",
{x: geometry.w - cornerSize, y: geometry.h + config.ui.titleBarHeight - cornerSize, w: cornerSize, h: cornerSize}, 2);
SAGE2Items.portals.interactMgr[dataSession.id] = new InteractableManager();
SAGE2Items.portals.interactMgr[dataSession.id].addLayer("radialMenus", 2);
SAGE2Items.portals.interactMgr[dataSession.id].addLayer("widgets", 1);
SAGE2Items.portals.interactMgr[dataSession.id].addLayer("applications", 0);
broadcast('initializeDataSharingSession', dataSession);
var key;
for (key in sagePointers) {
remoteWSIO.emit('createRemoteSagePointer', {id: key, portal: {host: config.host, port: config.port}});
}
var to = caller ? remoteTime.getTime() - Date.now() : 0;
remoteSharingSessions[dataSession.id] = {portal: dataSession, wsio: remoteWSIO, appCount: 0, timeOffset: to};
}
// Disabling data sharing portal for now
/*
function requestNewDataSharingSession(remote) {
return;
if (remote.connected) {
console.log("Requesting data-sharing session with " + remote.name);
remoteSharingWaitDialog = remote;
broadcast('dataSharingConnectionWait', {name: remote.name, host: remote.wsio.remoteAddress.address,
port: remote.wsio.remoteAddress.port});
remote.wsio.emit('requestDataSharingSession', {config: config, secure: false});
showWaitDialog(true);
} else {
console.log("Remote site " + remote.name + " is not currently connected");
}
}
*/
function showWaitDialog(flag) {
interactMgr.editVisibility("dataSharingWaitDialog", "staticUI", flag);
interactMgr.editVisibility("cancelDataSharingRequest", "staticUI", flag);
}
function showRequestDialog(flag) {
interactMgr.editVisibility("dataSharingRequestDialog", "staticUI", flag);
interactMgr.editVisibility("acceptDataSharingRequest", "staticUI", flag);
interactMgr.editVisibility("rejectDataSharingRequest", "staticUI", flag);
}
function pointerPressOnRadialMenu(uniqueID, pointerX, pointerY, data, obj, localPt, color) {
var existingRadialMenu = obj.data;
if (obj.id.indexOf("menu_radial_button") !== -1) {
// Pressing on radial menu button
var menuStateChange = existingRadialMenu.onButtonEvent(obj.id, uniqueID, "pointerPress", color);
if (menuStateChange !== undefined) {
radialMenuEvent({type: "stateChange", menuID: existingRadialMenu.id, menuState: menuStateChange });
}
} else if (obj.id.indexOf("menu_thumbnail") !== -1) {
// Pressing on thumbnail window
// console.log("Pointer press on thumbnail window");
data = { button: data.button, color: sagePointers[uniqueID].color };
radialMenuEvent({type: "pointerPress", id: uniqueID, x: pointerX, y: pointerY, data: data});
} else {
// Not on a button
// Drag Content Browser only from radial menu
if (data.button === "left" && obj.type !== 'rectangle') {
obj.data.onStartDrag(uniqueID, {x: pointerX, y: pointerY});
}
}
}
function pointerPressOrReleaseOnWidget(uniqueID, pointerX, pointerY, data, obj, localPt, pressRelease) {
var id = obj.data.id;
if (data.button === "left") {
var sidebarPoint = {x: obj.geometry.x - obj.data.left + localPt.x, y: obj.geometry.y - obj.data.top + localPt.y};
var btn = SAGE2Items.widgets.findButtonByPoint(id, localPt) || SAGE2Items.widgets.findButtonByPoint(id, sidebarPoint);
var ctrlData = {ctrlId: btn ? btn.id : null, appId: obj.data.appId, instanceID: id};
var regTI = /textInput/;
var regSl = /slider/;
var regButton = /button/;
var lockedControl = null;
var eUser = {id: sagePointers[uniqueID].id, label: sagePointers[uniqueID].label, color: sagePointers[uniqueID].color};
if (pressRelease === "press") {
// var textInputOrSlider = SAGE2Items.widgets.findButtonByPoint(id, sidebarPoint);
if (btn === null) {// && textInputOrSlider===null) {
remoteInteraction[uniqueID].selectMoveControl(obj.data, pointerX, pointerY);
} else {
remoteInteraction[uniqueID].releaseControl();
lockedControl = remoteInteraction[uniqueID].lockedControl();
if (lockedControl) {
// If a text input widget was locked, drop it
broadcast('deactivateTextInputControl', lockedControl);
remoteInteraction[uniqueID].dropControl();
}
remoteInteraction[uniqueID].lockControl(ctrlData);
if (regSl.test(btn.id)) {
broadcast('sliderKnobLockAction', {ctrl: ctrlData, x: pointerX, user: eUser, date: Date.now()});
} else if (regTI.test(btn.id)) {
broadcast('activateTextInputControl', {
prevTextInput: lockedControl,
curTextInput: ctrlData, date: Date.now()
});
}
}
} else {
lockedControl = remoteInteraction[uniqueID].lockedControl();
if (lockedControl !== null && btn !== null && regButton.test(btn.id) && lockedControl.ctrlId === btn.id) {
remoteInteraction[uniqueID].dropControl();
broadcast('executeControlFunction', {ctrl: ctrlData, user: eUser, date: Date.now()}, 'receivesWidgetEvents');
var app = SAGE2Items.applications.list[ctrlData.appId];
if (app) {
if (btn.id.indexOf("buttonCloseApp") >= 0) {
addEventToUserLog(data.addr, {type: "delete", data: {application:
{id: app.id, type: app.application}}, time: Date.now()});
} else if (btn.id.indexOf("buttonCloseWidget") >= 0) {
addEventToUserLog(data.addr, {type: "widgetMenu", data: {action: "close", application:
{id: app.id, type: app.application}}, time: Date.now()});
} else if (btn.id.indexOf("buttonShareApp") >= 0) {
console.log("sharing app");
} else {
addEventToUserLog(data.addr, {type: "widgetAction", data: {application:
data.appId, widget: data.ctrlId}, time: Date.now()});
}
}
}
remoteInteraction[uniqueID].releaseControl();
}
} else {
if (obj.data.show === true && pressRelease === "press") {
hideControl(obj.data);
var app2 = SAGE2Items.applications.list[obj.data.appId];
if (app2 !== null) {
addEventToUserLog(uniqueID, {type: "widgetMenu", data: {action: "close", application:
{id: app2.id, type: app2.application}}, time: Date.now()});
}
}
}
}
function releaseSlider(uniqueID) {
var ctrlData = remoteInteraction[uniqueID].lockedControl();
if (/slider/.test(ctrlData.ctrlId) === true) {
remoteInteraction[uniqueID].dropControl();
var eUser = {id: sagePointers[uniqueID].id, label: sagePointers[uniqueID].label, color: sagePointers[uniqueID].color};
broadcast('executeControlFunction', {ctrl: ctrlData, user: eUser}, 'receivesWidgetEvents');
}
}
function pointerPressOnApplication(uniqueID, pointerX, pointerY, data, obj, localPt, portalId) {
var im = findInteractableManager(obj.data.id);
im.moveObjectToFront(obj.id, "applications", ["portals"]);
var app = SAGE2Items.applications.list[obj.id];
var stickyList = stickyAppHandler.getStickingItems(app);
for (var idx in stickyList) {
im.moveObjectToFront(stickyList[idx].id, "applications", ["portals"]);
}
var newOrder = im.getObjectZIndexList("applications", ["portals"]);
broadcast('updateItemOrder', newOrder);
if (portalId !== undefined && portalId !== null) {
var ts = Date.now() + remoteSharingSessions[portalId].timeOffset;
remoteSharingSessions[portalId].wsio.emit('updateApplicationOrder', {order: newOrder, date: ts});
}
var btn = SAGE2Items.applications.findButtonByPoint(obj.id, localPt);
// pointer press on app window
if (btn === null) {
if (data.button === "right") {
var elemCtrl = SAGE2Items.widgets.list[obj.id + uniqueID + "_controls"];
if (!elemCtrl) {
// if no UI element, send event to app if in interaction mode
if (remoteInteraction[uniqueID].appInteractionMode()) {
sendPointerPressToApplication(uniqueID, obj.data, pointerX, pointerY, data);
}
// Request a control (do not know in advance)
broadcast('requestNewControl', {elemId: obj.id, user_id: uniqueID,
user_label: sagePointers[uniqueID] ? sagePointers[uniqueID].label : "",
x: pointerX, y: pointerY, date: Date.now() });
} else if (elemCtrl.show === false) {
showControl(elemCtrl, uniqueID, pointerX, pointerY);
addEventToUserLog(uniqueID, {type: "widgetMenu", data: {action: "open", application:
{id: obj.id, type: obj.data.application}}, time: Date.now()});
} else {
moveControlToPointer(elemCtrl, uniqueID, pointerX, pointerY);
}
} else {
if (remoteInteraction[uniqueID].appInteractionMode()) {
sendPointerPressToApplication(uniqueID, obj.data, pointerX, pointerY, data);
} else {
selectApplicationForMove(uniqueID, obj.data, pointerX, pointerY, portalId);
}
}
return;
}
switch (btn.id) {
case "titleBar":
if (drawingManager.paletteID !== uniqueID) {
selectApplicationForMove(uniqueID, obj.data, pointerX, pointerY, portalId);
}
break;
case "dragCorner":
if (obj.data.application === "Webview") {
// resize with corner only in window mode
if (!sagePointers[uniqueID].visible || remoteInteraction[uniqueID].windowManagementMode()) {
selectApplicationForResize(uniqueID, obj.data, pointerX, pointerY, portalId);
} else {
// if corner click and webview, then send the click to app
sendPointerPressToApplication(uniqueID, obj.data, pointerX, pointerY, data);
}
} else {
selectApplicationForResize(uniqueID, obj.data, pointerX, pointerY, portalId);
}
break;
case "syncButton":
if (sagePointers[uniqueID].visible) {
// only if pointer on the wall, not the web UI
broadcast('toggleSyncOptions', {id: obj.data.id});
}
break;
case "fullscreenButton":
if (sagePointers[uniqueID].visible) {
// only if pointer on the wall, not the web UI
toggleApplicationFullscreen(uniqueID, obj.data, portalId);
}
break;
case "pinButton":
if (sagePointers[uniqueID].visible) {
// only if pointer on the wall, not the web UI
toggleStickyPin(obj.data.id);
}
break;
case "closeButton":
if (sagePointers[uniqueID].visible) {
// only if pointer on the wall, not the web UI
deleteApplication(obj.data.id, portalId);
}
break;
}
}
function pointerPressOnPartition(uniqueID, pointerX, pointerY, data, obj, localPt, portalId) {
var btn = partitions.findButtonByPoint(obj.id, localPt);
// pointer press on ptn window
if (btn === null) {
if (data.button === "left") {
// control drag on partition begins cutting action
if (sagePointers[uniqueID].visible && remoteInteraction[uniqueID].CTRL) {
// start tracking size to create new partition
cuttingPartition[uniqueID] = {};
cuttingPartition[uniqueID].start = {x: pointerX, y: pointerY};
cuttingPartition[uniqueID].ptn = obj.data;
} else {
selectApplicationForMove(uniqueID, obj.data, pointerX, pointerY);
}
}
return;
}
switch (btn.id) {
case "titleBar":
selectApplicationForMove(uniqueID, obj.data, pointerX, pointerY);
break;
case "dragCorner":
selectApplicationForResize(uniqueID, obj.data, pointerX, pointerY, portalId);
break;
case "tileButton":
if (sagePointers[uniqueID].visible) {
var changedPartitions = partitions.list[obj.id].toggleInnerTiling();
updatePartitionInnerLayout(partitions.list[obj.id], true);
changedPartitions.forEach(el => {
broadcast('partitionWindowTitleUpdate', partitions.list[el].getTitle());
});
}
break;
case "clearButton":
if (sagePointers[uniqueID].visible) {
// only if pointer on the wall, not the web UI
// clear partition (close all windows inside)
if (partitions.list.hasOwnProperty(obj.id)) {
// passing method to delete applications for use within clearPartition method
changedPartitions = partitions.list[obj.id].clearPartition(deleteApplication);
changedPartitions.forEach(el => {
broadcast('partitionWindowTitleUpdate', partitions.list[el].getTitle());
});
}
}
break;
case "fullscreenButton":
if (sagePointers[uniqueID].visible) {
// only if pointer on the wall, not the web UI
if (!obj.data.maximized) {
remoteInteraction[uniqueID].maximizeSelectedItem(obj.data);
} else {
remoteInteraction[uniqueID].restoreSelectedItem(obj.data);
}
partitions.updatePartitionGeometries(obj.id, interactMgr);
broadcast('partitionMoveAndResizeFinished', obj.data.getDisplayInfo());
// update neighbors if it is snapped
if (obj.data.isSnapping) {
let updatedNeighbors = obj.data.updateNeighborPtnPositions();
// update geometries/display/layout of any updated neighbors
for (var neigh of updatedNeighbors) {
partitions.updatePartitionGeometries(neigh, interactMgr);
broadcast('partitionMoveAndResizeFinished', partitions.list[neigh].getDisplayInfo());
updatePartitionInnerLayout(partitions.list[neigh], true);
}
}
// update child positions within partiton
updatePartitionInnerLayout(partitions.list[obj.id], false);
}
break;
case "closeButton":
if (sagePointers[uniqueID].visible) {
// only if pointer on the wall, not the web UI
deletePartition(obj.id);
}
break;
}
}
function pointerPressOnDataSharingPortal(uniqueID, pointerX, pointerY, data, obj, localPt) {
interactMgr.moveObjectToFront(obj.id, "portals", ["applications"]);
var newOrder = interactMgr.getObjectZIndexList("portals", ["applications"]);
broadcast('updateItemOrder', newOrder);
var btn = SAGE2Items.portals.findButtonByPoint(obj.id, localPt);
// pointer press inside portal window
if (btn === null) {
var scaledPt = {x: localPt.x / obj.data.scale, y: (localPt.y - config.ui.titleBarHeight) / obj.data.scale};
pointerPressInDataSharingArea(uniqueID, obj.data.id, scaledPt, data);
return;
}
switch (btn.id) {
case "titleBar": {
selectPortalForMove(uniqueID, obj.data, pointerX, pointerY);
break;
}
case "dragCorner": {
if (remoteInteraction[uniqueID].windowManagementMode()) {
selectPortalForResize(uniqueID, obj.data, pointerX, pointerY);
}
break;
}
case "fullscreenButton": {
// toggleApplicationFullscreen(uniqueID, obj.data);
break;
}
case "pinButton": {
// toggleStickyPin(obj.data.id);
break;
}
case "closeButton": {
// deleteApplication(obj.data.id);
break;
}
}
}
function pointerPressInDataSharingArea(uniqueID, portalId, scaledPt, data) {
var pObj = SAGE2Items.portals.interactMgr[portalId].searchGeometry(scaledPt);
if (pObj === null) {
// pointerPressOnOpenSpace(uniqueID, pointerX, pointerY, data);
return;
}
var pLocalPt = globalToLocal(scaledPt.x, scaledPt.y, pObj.type, pObj.geometry);
switch (pObj.layerId) {
case "radialMenus": {
// pointerPressOnRadialMenu(uniqueID, pointerX, pointerY, data, pObj, pLocalPt);
break;
}
case "widgets": {
// pointerPressOnWidget(uniqueID, pointerX, pointerY, data, pObj, pLocalPt);
break;
}
case "applications": {
pointerPressOnApplication(uniqueID, scaledPt.x, scaledPt.y, data, pObj, pLocalPt, portalId);
break;
}
}
return;
}
function selectApplicationForMove(uniqueID, app, pointerX, pointerY, portalId) {
remoteInteraction[uniqueID].selectMoveItem(app, pointerX, pointerY);
broadcast('startMove', {id: app.id, date: Date.now()});
if (portalId !== undefined && portalId !== null) {
var ts = Date.now() + remoteSharingSessions[portalId].timeOffset;
remoteSharingSessions[portalId].wsio.emit('startApplicationMove', {id: uniqueID, appId: app.id, date: ts});
}
var eLogData = {
type: "move",
action: "start",
application: {
id: app.id,
type: app.application
},
location: {
x: parseInt(app.left, 10),
y: parseInt(app.top, 10),
width: parseInt(app.width, 10),
height: parseInt(app.height, 10)
}
};
addEventToUserLog(uniqueID, {type: "windowManagement", data: eLogData, time: Date.now()});
}
function selectApplicationForResize(uniqueID, app, pointerX, pointerY, portalId) {
remoteInteraction[uniqueID].selectResizeItem(app, pointerX, pointerY);
broadcast('startResize', {id: app.id, date: Date.now()});
if (portalId !== undefined && portalId !== null) {
var ts = Date.now() + remoteSharingSessions[portalId].timeOffset;
remoteSharingSessions[portalId].wsio.emit('startApplicationResize', {id: uniqueID, appId: app.id, date: ts});
}
var eLogData = {
type: "resize",
action: "start",
application: {
id: app.id,
type: app.application
},
location: {
x: parseInt(app.left, 10),
y: parseInt(app.top, 10),
width: parseInt(app.width, 10),
height: parseInt(app.height, 10)
}
};
addEventToUserLog(uniqueID, {type: "windowManagement", data: eLogData, time: Date.now()});
}
function sendPointerPressToApplication(uniqueID, app, pointerX, pointerY, data) {
var ePosition = {x: pointerX - app.left, y: pointerY - (app.top + config.ui.titleBarHeight)};
var eUser = {id: sagePointers[uniqueID].id, label: sagePointers[uniqueID].label, color: sagePointers[uniqueID].color};
var event = {
id: app.id,
type: "pointerPress",
position: ePosition,
user: eUser,
data: data,
date: Date.now()
};
handleStickyItem(app.id);
broadcast('eventInItem', event);
var eLogData = {
type: "pointerPress",
application: {
id: app.id,
type: app.application
},
position: {
x: parseInt(ePosition.x, 10),
y: parseInt(ePosition.y, 10)
}
};
addEventToUserLog(uniqueID, {type: "applicationInteraction", data: eLogData, time: Date.now()});
}
function sendPointerDblClickToApplication(uniqueID, app, pointerX, pointerY) {
var ePosition = {x: pointerX - app.left, y: pointerY - (app.top + config.ui.titleBarHeight)};
var eUser = {id: sagePointers[uniqueID].id, label: sagePointers[uniqueID].label, color: sagePointers[uniqueID].color};
var event = {
id: app.id,
type: "pointerDblClick",
position: ePosition,
user: eUser,
date: Date.now()
};
broadcast('eventInItem', event);
var eLogData = {
type: "pointerDblClick",
application: {
id: app.id,
type: app.application
},
position: {
x: parseInt(ePosition.x, 10),
y: parseInt(ePosition.y, 10)
}
};
addEventToUserLog(uniqueID, {type: "applicationInteraction", data: eLogData, time: Date.now()});
}
function selectPortalForMove(uniqueID, portal, pointerX, pointerY) {
remoteInteraction[uniqueID].selectMoveItem(portal, pointerX, pointerY);
var eLogData = {
type: "move",
action: "start",
portal: {
id: portal.id,
name: portal.name,
host: portal.host,
port: portal.port
},
location: {
x: parseInt(portal.left, 10),
y: parseInt(portal.top, 10),
width: parseInt(portal.width, 10),
height: parseInt(portal.height, 10)
}
};
addEventToUserLog(uniqueID, {type: "windowManagement", data: eLogData, time: Date.now()});
}
function selectPortalForResize(uniqueID, portal, pointerX, pointerY) {
remoteInteraction[uniqueID].selectResizeItem(portal, pointerX, pointerY);
var eLogData = {
type: "resize",
action: "start",
portal: {
id: portal.id,
name: portal.name,
host: portal.host,
port: portal.port
},
location: {
x: parseInt(portal.left, 10),
y: parseInt(portal.top, 10),
width: parseInt(portal.width, 10),
height: parseInt(portal.height, 10)
}
};
addEventToUserLog(uniqueID, {type: "windowManagement", data: eLogData, time: Date.now()});
}
function pointerMove(uniqueID, pointerX, pointerY, data) {
if (sagePointers[uniqueID] === undefined) {
return;
}
// Whiteboard app
if (drawingManager.drawingMode) {
var color = sagePointers[uniqueID] ? sagePointers[uniqueID].color : null;
drawingManager.pointerEvent(
omicronManager.sageToOmicronEvent(uniqueID, pointerX, pointerY, data, 4, color),
uniqueID, pointerX, pointerY, 10, 10);
}
// Trick: press CTRL key while moving switches interaction mode
if (sagePointers[uniqueID] && remoteInteraction[uniqueID].CTRL && pressingCTRL) {
remoteInteraction[uniqueID].toggleModes();
broadcast('changeSagePointerMode', {id: sagePointers[uniqueID].id, mode: remoteInteraction[uniqueID].interactionMode});
pressingCTRL = false;
} else if (sagePointers[uniqueID] && !remoteInteraction[uniqueID].CTRL && !pressingCTRL) {
remoteInteraction[uniqueID].toggleModes();
broadcast('changeSagePointerMode', {id: sagePointers[uniqueID].id, mode: remoteInteraction[uniqueID].interactionMode});
pressingCTRL = true;
}
sagePointers[uniqueID].updatePointerPosition(data, config.totalWidth, config.totalHeight);
pointerX = sagePointers[uniqueID].left;
pointerY = sagePointers[uniqueID].top;
updatePointerPosition(uniqueID, pointerX, pointerY, data);
}
function pointerPosition(uniqueID, data) {
if (sagePointers[uniqueID] === undefined) {
return;
}
sagePointers[uniqueID].updatePointerPosition(data, config.totalWidth, config.totalHeight);
var pointerX = sagePointers[uniqueID].left;
var pointerY = sagePointers[uniqueID].top;
updatePointerPosition(uniqueID, pointerX, pointerY, data);
}
function updatePointerPosition(uniqueID, pointerX, pointerY, data) {
broadcast('updateSagePointerPosition', sagePointers[uniqueID]);
var localPt;
var scaledPt;
var moveAppPortal = findApplicationPortal(remoteInteraction[uniqueID].selectedMoveItem);
var resizeAppPortal = findApplicationPortal(remoteInteraction[uniqueID].selectedResizeItem);
var updatedMoveItem;
var updatedResizeItem;
var updatedControl;
if (draggingPartition[uniqueID]) {
draggingPartition[uniqueID].ptn.left =
pointerX < draggingPartition[uniqueID].start.x ?
pointerX : draggingPartition[uniqueID].start.x;
draggingPartition[uniqueID].ptn.top =
pointerY < draggingPartition[uniqueID].start.y ?
pointerY : draggingPartition[uniqueID].start.y;
draggingPartition[uniqueID].ptn.width =
pointerX < draggingPartition[uniqueID].start.x ?
draggingPartition[uniqueID].start.x - pointerX : pointerX - draggingPartition[uniqueID].start.x;
draggingPartition[uniqueID].ptn.height =
pointerY < draggingPartition[uniqueID].start.y ?
draggingPartition[uniqueID].start.y - pointerY : pointerY - draggingPartition[uniqueID].start.y;
partitions.updatePartitionGeometries(draggingPartition[uniqueID].ptn.id, interactMgr);
broadcast('partitionMoveAndResizeFinished', draggingPartition[uniqueID].ptn.getDisplayInfo());
}
// if the user is cutting a partition
if (cuttingPartition[uniqueID]) {
var cutDirection = Math.abs(pointerX - cuttingPartition[uniqueID].start.x) >
Math.abs(pointerY - cuttingPartition[uniqueID].start.y) ?
"horizontal" : "vertical";
var cutPosition = cutDirection === "horizontal" ?
(cuttingPartition[uniqueID].start.y + pointerY) / 2 :
(cuttingPartition[uniqueID].start.x + pointerX) / 2;
var cutDist = Math.sqrt(Math.pow(pointerY - cuttingPartition[uniqueID].start.y, 2) +
Math.pow(pointerX - cuttingPartition[uniqueID].start.x, 2));
var oldPtn = cuttingPartition[uniqueID].ptn;
// calculate dimensions of new partitions
var newDims1, newDims2 = null;
if (cutDirection === "horizontal") {
// make sure partition is tall enough to split
if (oldPtn.height < 2 * partitions.minSize.height) {
return;
}
// clamp cut position inside partition so it doesn't break
if (cutPosition > (oldPtn.top + oldPtn.height - partitions.minSize.height)) {
cutPosition = oldPtn.top + oldPtn.height - partitions.minSize.height;
}
if (cutPosition < (oldPtn.top + partitions.minSize.height)) {
cutPosition = oldPtn.top + partitions.minSize.height;
}
newDims1 = {
top: oldPtn.top,
left: oldPtn.left,
width: oldPtn.width,
height: cutPosition - oldPtn.top - config.ui.titleBarHeight
};
newDims2 = {
top: cutPosition,
left: oldPtn.left,
width: oldPtn.width,
height: (oldPtn.top + oldPtn.height) - cutPosition
};
} else if (cutDirection === "vertical") {
// make sure partition is wide enough to split
if (oldPtn.width < 2 * partitions.minSize.width) {
return;
}
// clamp cut position inside partition so it doesn't break
if (cutPosition > (oldPtn.left + oldPtn.width - partitions.minSize.width)) {
cutPosition = oldPtn.left + oldPtn.width - partitions.minSize.width;
}
if (cutPosition < (oldPtn.left + partitions.minSize.width)) {
cutPosition = oldPtn.left + partitions.minSize.width;
}
newDims1 = {
top: oldPtn.top,
left: oldPtn.left,
width: cutPosition - oldPtn.left,
height: oldPtn.height
};
newDims2 = {
top: oldPtn.top,
left: cutPosition,
width: (oldPtn.left + oldPtn.width) - cutPosition,
height: oldPtn.height
};
}
if (cutDist > Math.min(oldPtn.width, oldPtn.height) / 3) {
// if partitions are not created
if (!cuttingPartition[uniqueID].newPtn1 && !cuttingPartition[uniqueID].newPtn2) {
// if the gesture is long enough and the new partitions haven't been made, create them
var ptnColor = oldPtn.color;
// create the 2 new partitions
cuttingPartition[uniqueID].newPtn1 = createPartition(newDims1, ptnColor);
cuttingPartition[uniqueID].newPtn2 = createPartition(newDims2, ptnColor);
broadcast('updatePartitionBorders', {id: cuttingPartition[uniqueID].newPtn1.id, highlight: true});
broadcast('updatePartitionBorders', {id: cuttingPartition[uniqueID].newPtn2.id, highlight: true});
} else {
// if they are already created just update their size and position
// resize partition 1
cuttingPartition[uniqueID].newPtn1.left = newDims1.left;
cuttingPartition[uniqueID].newPtn1.top = newDims1.top;
cuttingPartition[uniqueID].newPtn1.width = newDims1.width;
cuttingPartition[uniqueID].newPtn1.height = newDims1.height;
// resize partition 2
cuttingPartition[uniqueID].newPtn2.left = newDims2.left;
cuttingPartition[uniqueID].newPtn2.top = newDims2.top;
cuttingPartition[uniqueID].newPtn2.width = newDims2.width;
cuttingPartition[uniqueID].newPtn2.height = newDims2.height;
moveAndResizePartitionWindow(uniqueID, {elemId: cuttingPartition[uniqueID].newPtn1.id});
moveAndResizePartitionWindow(uniqueID, {elemId: cuttingPartition[uniqueID].newPtn2.id});
}
}
}
if (moveAppPortal !== null) {
localPt = globalToLocal(pointerX, pointerY, moveAppPortal.type, moveAppPortal.geometry);
scaledPt = {x: localPt.x / moveAppPortal.data.scale,
y: (localPt.y - config.ui.titleBarHeight) / moveAppPortal.data.scale};
remoteSharingSessions[moveAppPortal.id].wsio.emit('remoteSagePointerPosition',
{id: uniqueID, left: scaledPt.x, top: scaledPt.y});
updatedMoveItem = remoteInteraction[uniqueID].moveSelectedItem(scaledPt.x, scaledPt.y);
moveApplicationWindow(uniqueID, updatedMoveItem, moveAppPortal.id);
return;
}
if (resizeAppPortal !== null) {
localPt = globalToLocal(pointerX, pointerY, resizeAppPortal.type, resizeAppPortal.geometry);
scaledPt = {x: localPt.x / resizeAppPortal.data.scale,
y: (localPt.y - config.ui.titleBarHeight) / resizeAppPortal.data.scale};
remoteSharingSessions[resizeAppPortal.id].wsio.emit('remoteSagePointerPosition',
{id: uniqueID, left: scaledPt.x, top: scaledPt.y});
updatedResizeItem = remoteInteraction[uniqueID].resizeSelectedItem(scaledPt.x, scaledPt.y);
moveAndResizeApplicationWindow(updatedResizeItem, resizeAppPortal.id);
return;
}
// update radial menu position if dragged outside radial menu
updateRadialMenuPointerPosition(uniqueID, pointerX, pointerY);
// update app position and size if currently modifying a window
updatedMoveItem = remoteInteraction[uniqueID].moveSelectedItem(pointerX, pointerY);
updatedResizeItem = remoteInteraction[uniqueID].resizeSelectedItem(pointerX, pointerY);
updatedControl = remoteInteraction[uniqueID].moveSelectedControl(pointerX, pointerY);
if (updatedMoveItem !== null) {
if (SAGE2Items.portals.list.hasOwnProperty(updatedMoveItem.elemId)) {
moveDataSharingPortalWindow(updatedMoveItem);
} else if (partitions.list.hasOwnProperty(updatedMoveItem.elemId)) {
moveAndResizePartitionWindow(uniqueID, updatedMoveItem, null);
} else {
moveApplicationWindow(uniqueID, updatedMoveItem, null);
let currentMoveItem = SAGE2Items.applications.list[updatedMoveItem.elemId];
if (currentMoveItem) {
// Calculate partition which item is over
let newPartitionHovered = partitions.calculateNewPartition(currentMoveItem, {x: pointerX, y: pointerY});
if (currentMoveItem.ptnHovered != newPartitionHovered) {
broadcast('updatePartitionBorders', {id: currentMoveItem.ptnHovered, highlight: false});
// update ptnHovered with new partition
currentMoveItem.ptnHovered = newPartitionHovered;
broadcast('updatePartitionBorders', {id: currentMoveItem.ptnHovered, highlight: true});
}
}
}
return;
}
if (updatedResizeItem !== null) {
if (SAGE2Items.portals.list.hasOwnProperty(updatedResizeItem.elemId)) {
moveAndResizeDataSharingPortalWindow(updatedResizeItem);
} else if (partitions.list.hasOwnProperty(updatedResizeItem.elemId)) {
moveAndResizePartitionWindow(uniqueID, updatedResizeItem, null);
} else {
moveAndResizeApplicationWindow(updatedResizeItem, null);
}
return;
}
if (updatedControl !== null) {
moveWidgetControls(uniqueID, updatedControl);
return;
}
var prevInteractionItem = remoteInteraction[uniqueID].getPreviousInteractionItem();
var obj = interactMgr.searchGeometry({x: pointerX, y: pointerY});
if (obj === null) {
removeExistingHoverCorner(uniqueID);
if (remoteInteraction[uniqueID].portal !== null) {
remoteSharingSessions[remoteInteraction[uniqueID].portal.id].wsio.emit('stopRemoteSagePointer', {id: uniqueID});
remoteInteraction[uniqueID].portal = null;
}
if (prevInteractionItem !== null) {
showOrHideWidgetLinks({uniqueID: uniqueID, item: prevInteractionItem, show: false});
}
} else {
var color = sagePointers[uniqueID] ? sagePointers[uniqueID].color : null;
if (prevInteractionItem !== obj) {
if (prevInteractionItem !== null) {
showOrHideWidgetLinks({uniqueID: uniqueID, item: prevInteractionItem, show: false});
}
showOrHideWidgetLinks({uniqueID: uniqueID, item: obj, user_color: color, show: true});
} else {
var appId = obj.id;
if (obj.data !== undefined && obj.data !== null && obj.data.appId !== undefined) {
appId = obj.data.appId;
}
if (appUserColors[appId] !== color) {
showOrHideWidgetLinks({uniqueID: uniqueID, item: prevInteractionItem, show: false});
showOrHideWidgetLinks({uniqueID: uniqueID, item: obj, user_color: color, show: true});
}
}
localPt = globalToLocal(pointerX, pointerY, obj.type, obj.geometry);
switch (obj.layerId) {
case "staticUI": {
removeExistingHoverCorner(uniqueID);
if (remoteInteraction[uniqueID].portal !== null) {
remoteSharingSessions[remoteInteraction[uniqueID].portal.id].wsio.emit(
'stopRemoteSagePointer', {id: uniqueID});
remoteInteraction[uniqueID].portal = null;
}
break;
}
case "radialMenus": {
pointerMoveOnRadialMenu(uniqueID, pointerX, pointerY, data, obj, localPt, color);
removeExistingHoverCorner(uniqueID);
if (remoteInteraction[uniqueID].portal !== null) {
remoteSharingSessions[remoteInteraction[uniqueID].portal.id].wsio.emit(
'stopRemoteSagePointer', {id: uniqueID});
remoteInteraction[uniqueID].portal = null;
}
break;
}
case "widgets": {
pointerMoveOnWidgets(uniqueID, pointerX, pointerY, data, obj, localPt);
removeExistingHoverCorner(uniqueID);
if (remoteInteraction[uniqueID].portal !== null) {
remoteSharingSessions[remoteInteraction[uniqueID].portal.id].wsio.emit(
'stopRemoteSagePointer', {id: uniqueID});
remoteInteraction[uniqueID].portal = null;
}
break;
}
case "applications": {
pointerMoveOnApplication(uniqueID, pointerX, pointerY, data, obj, localPt, null);
if (remoteInteraction[uniqueID].portal !== null) {
remoteSharingSessions[remoteInteraction[uniqueID].portal.id].wsio.emit(
'stopRemoteSagePointer', {id: uniqueID});
remoteInteraction[uniqueID].portal = null;
}
break;
}
case "partitions": {
pointerMoveOnPartition(uniqueID, pointerX, pointerY, data, obj, localPt, null);
break;
}
case "portals": {
pointerMoveOnDataSharingPortal(uniqueID, pointerX, pointerY, data, obj, localPt);
break;
}
}
}
remoteInteraction[uniqueID].setPreviousInteractionItem(obj);
}
function pointerMoveOnRadialMenu(uniqueID, pointerX, pointerY, data, obj, localPt, color) {
var existingRadialMenu = obj.data;
if (obj.id.indexOf("menu_radial_button") !== -1) {
// Pressing on radial menu button
// console.log("over radial button: " + obj.id);
// data = { buttonID: obj.id, button: data.button, color: sagePointers[uniqueID].color };
// radialMenuEvent({type: "pointerMove", id: uniqueID, x: pointerX, y: pointerY, data: data});
var menuStateChange = existingRadialMenu.onButtonEvent(obj.id, uniqueID, "pointerMove", color);
if (menuStateChange !== undefined) {
radialMenuEvent({type: "stateChange", menuID: existingRadialMenu.id, menuState: menuStateChange });
}
} else if (obj.id.indexOf("menu_thumbnail") !== -1) {
// PointerMove on thumbnail window
// console.log("Pointer move on thumbnail window");
data = { button: data.button, color: sagePointers[uniqueID].color };
radialMenuEvent({type: "pointerMove", id: uniqueID, x: pointerX, y: pointerY, data: data});
} else {
// Not on a button
var menuButtonState = existingRadialMenu.onMenuEvent(uniqueID);
if (menuButtonState !== undefined) {
radialMenuEvent({type: "stateChange", menuID: existingRadialMenu.id, menuState: menuButtonState });
}
// Drag Content Browser only from radial menu
if (existingRadialMenu.dragState === true && obj.type !== 'rectangle') {
var offset = existingRadialMenu.getDragOffset(uniqueID, {x: pointerX, y: pointerY});
moveRadialMenu(existingRadialMenu.id, offset.x, offset.y);
radialMenuEvent({type: "pointerMove", id: uniqueID, x: pointerX, y: pointerY, data: data});
}
}
}
function pointerMoveOnWidgets(uniqueID, pointerX, pointerY, data, obj, localPt) {
// widgets
var lockedControl = remoteInteraction[uniqueID].lockedControl();
var eUser = {id: sagePointers[uniqueID].id, label: sagePointers[uniqueID].label, color: sagePointers[uniqueID].color};
if (lockedControl && /slider/.test(lockedControl.ctrlId)) {
broadcast('moveSliderKnob', {ctrl: lockedControl, x: pointerX, user: eUser, date: Date.now()});
return;
}
// showOrHideWidgetConnectors(uniqueID, obj.data, "move");
// Widget connector show logic ends
}
function pointerMoveOnApplication(uniqueID, pointerX, pointerY, data, obj, localPt, portalId) {
var btn = SAGE2Items.applications.findButtonByPoint(obj.id, localPt);
// pointer move on app window
if (btn === null) {
removeExistingHoverCorner(uniqueID, portalId);
if (remoteInteraction[uniqueID].appInteractionMode()) {
sendPointerMoveToApplication(uniqueID, obj.data, pointerX, pointerY, data);
}
return;
}
var ts;
switch (btn.id) {
case "titleBar": {
removeExistingHoverCorner(uniqueID, portalId);
break;
}
case "dragCorner": {
if (obj.data.application === "Webview") {
// resize corner only in window mode
if (!sagePointers[uniqueID].visible || remoteInteraction[uniqueID].windowManagementMode()) {
if (remoteInteraction[uniqueID].hoverCornerItem === null) {
remoteInteraction[uniqueID].setHoverCornerItem(obj.data);
broadcast('hoverOverItemCorner', {elemId: obj.data.id, flag: true});
if (portalId !== undefined && portalId !== null) {
ts = Date.now() + remoteSharingSessions[portalId].timeOffset;
remoteSharingSessions[portalId].wsio.emit('remoteSagePointerHoverCorner',
{appHoverCorner: {elemId: obj.data.id, flag: true}, date: ts});
}
} else if (remoteInteraction[uniqueID].hoverCornerItem.id !== obj.data.id) {
broadcast('hoverOverItemCorner', {elemId: remoteInteraction[uniqueID].hoverCornerItem.id, flag: false});
if (portalId !== undefined && portalId !== null) {
ts = Date.now() + remoteSharingSessions[portalId].timeOffset;
remoteSharingSessions[portalId].wsio.emit('remoteSagePointerHoverCorner',
{appHoverCorner: {elemId: remoteInteraction[uniqueID].hoverCornerItem.id,
flag: false}, date: ts});
}
remoteInteraction[uniqueID].setHoverCornerItem(obj.data);
broadcast('hoverOverItemCorner', {elemId: obj.data.id, flag: true});
if (portalId !== undefined && portalId !== null) {
ts = Date.now() + remoteSharingSessions[portalId].timeOffset;
remoteSharingSessions[portalId].wsio.emit('remoteSagePointerHoverCorner',
{appHoverCorner: {elemId: obj.data.id, flag: true}, date: ts});
}
}
}
} else {
if (remoteInteraction[uniqueID].hoverCornerItem === null) {
remoteInteraction[uniqueID].setHoverCornerItem(obj.data);
broadcast('hoverOverItemCorner', {elemId: obj.data.id, flag: true});
if (portalId !== undefined && portalId !== null) {
ts = Date.now() + remoteSharingSessions[portalId].timeOffset;
remoteSharingSessions[portalId].wsio.emit('remoteSagePointerHoverCorner',
{appHoverCorner: {elemId: obj.data.id, flag: true}, date: ts});
}
} else if (remoteInteraction[uniqueID].hoverCornerItem.id !== obj.data.id) {
broadcast('hoverOverItemCorner', {elemId: remoteInteraction[uniqueID].hoverCornerItem.id, flag: false});
if (portalId !== undefined && portalId !== null) {
ts = Date.now() + remoteSharingSessions[portalId].timeOffset;
remoteSharingSessions[portalId].wsio.emit('remoteSagePointerHoverCorner',
{appHoverCorner: {elemId: remoteInteraction[uniqueID].hoverCornerItem.id, flag: false}, date: ts});
}
remoteInteraction[uniqueID].setHoverCornerItem(obj.data);
broadcast('hoverOverItemCorner', {elemId: obj.data.id, flag: true});
if (portalId !== undefined && portalId !== null) {
ts = Date.now() + remoteSharingSessions[portalId].timeOffset;
remoteSharingSessions[portalId].wsio.emit('remoteSagePointerHoverCorner',
{appHoverCorner: {elemId: obj.data.id, flag: true}, date: ts});
}
}
}
break;
}
case "fullscreenButton": {
removeExistingHoverCorner(uniqueID, portalId);
break;
}
case "pinButton": {
removeExistingHoverCorner(uniqueID, portalId);
break;
}
case "closeButton": {
removeExistingHoverCorner(uniqueID, portalId);
break;
}
}
}
function pointerMoveOnPartition(uniqueID, pointerX, pointerY, data, obj, localPt, portalId) {
var btn = partitions.findButtonByPoint(obj.id, localPt);
// pointer press on app window
if (btn === null || draggingPartition[uniqueID]) {
return;
}
switch (btn.id) {
case "titleBar":
break;
case "dragCorner":
if (remoteInteraction[uniqueID].hoverCornerItem === null) {
remoteInteraction[uniqueID].setHoverCornerItem(obj.data);
broadcast('hoverOverItemCorner', {elemId: obj.data.id, flag: true});
} else if (remoteInteraction[uniqueID].hoverCornerItem.id !== obj.data.id) {
broadcast('hoverOverItemCorner', {elemId: remoteInteraction[uniqueID].hoverCornerItem.id, flag: false});
remoteInteraction[uniqueID].setHoverCornerItem(obj.data);
broadcast('hoverOverItemCorner', {elemId: obj.data.id, flag: true});
}
break;
case "tileButton":
break;
case "clearButton":
break;
case "fullscreenButton":
break;
case "closeButton":
break;
}
}
function pointerMoveOnDataSharingPortal(uniqueID, pointerX, pointerY, data, obj, localPt) {
var scaledPt = {x: localPt.x / obj.data.scale, y: (localPt.y - config.ui.titleBarHeight) / obj.data.scale};
if (remoteInteraction[uniqueID].portal === null || remoteInteraction[uniqueID].portal.id !== obj.data.id) {
remoteInteraction[uniqueID].portal = obj.data;
var rPointer = {
id: uniqueID,
left: scaledPt.x,
top: scaledPt.y,
label: sagePointers[uniqueID].label,
color: sagePointers[uniqueID].color
};
remoteSharingSessions[remoteInteraction[uniqueID].portal.id].wsio.emit('startRemoteSagePointer', rPointer);
}
remoteSharingSessions[obj.data.id].wsio.emit('remoteSagePointerPosition', {id: uniqueID, left: scaledPt.x, top: scaledPt.y});
var btn = SAGE2Items.portals.findButtonByPoint(obj.id, localPt);
// pointer move on portal window
if (btn === null) {
var pObj = SAGE2Items.portals.interactMgr[obj.data.id].searchGeometry(scaledPt);
if (pObj === null) {
removeExistingHoverCorner(uniqueID, obj.data.id);
return;
}
var pLocalPt = globalToLocal(scaledPt.x, scaledPt.y, pObj.type, pObj.geometry);
switch (pObj.layerId) {
case "radialMenus": {
removeExistingHoverCorner(uniqueID, obj.data.id);
break;
}
case "widgets": {
removeExistingHoverCorner(uniqueID, obj.data.id);
break;
}
case "applications": {
pointerMoveOnApplication(uniqueID, scaledPt.x, scaledPt.y, data, pObj, pLocalPt, obj.data.id);
break;
}
}
return;
}
switch (btn.id) {
case "titleBar": {
removeExistingHoverCorner(uniqueID, obj.data.id);
break;
}
case "dragCorner": {
if (remoteInteraction[uniqueID].windowManagementMode()) {
if (remoteInteraction[uniqueID].hoverCornerItem === null) {
remoteInteraction[uniqueID].setHoverCornerItem(obj.data);
broadcast('hoverOverItemCorner', {elemId: obj.data.id, flag: true});
} else if (remoteInteraction[uniqueID].hoverCornerItem.id !== obj.data.id) {
broadcast('hoverOverItemCorner', {elemId: remoteInteraction[uniqueID].hoverCornerItem.id, flag: false});
var ts = Date.now() + remoteSharingSessions[obj.data.id].timeOffset;
remoteSharingSessions[obj.data.id].wsio.emit('remoteSagePointerHoverCorner',
{appHoverCorner: {elemId: remoteInteraction[uniqueID].hoverCornerItem.id, flag: false}, date: ts});
remoteInteraction[uniqueID].setHoverCornerItem(obj.data);
broadcast('hoverOverItemCorner', {elemId: obj.data.id, flag: true});
}
} else if (remoteInteraction[uniqueID].appInteractionMode()) {
// sendPointerMoveToApplication(uniqueID, obj.data, pointerX, pointerY, data);
}
break;
}
case "fullscreenButton": {
removeExistingHoverCorner(uniqueID, obj.data.id);
break;
}
case "pinButton": {
removeExistingHoverCorner(uniqueID, obj.data.id);
break;
}
case "closeButton": {
removeExistingHoverCorner(uniqueID, obj.data.id);
break;
}
}
}
function removeExistingHoverCorner(uniqueID, portalId) {
// remove hover corner if exists
if (remoteInteraction[uniqueID].hoverCornerItem !== null) {
broadcast('hoverOverItemCorner', {elemId: remoteInteraction[uniqueID].hoverCornerItem.id, flag: false});
if (portalId !== undefined && portalId !== null) {
var ts = Date.now() + remoteSharingSessions[portalId].timeOffset;
remoteSharingSessions[portalId].wsio.emit('remoteSagePointerHoverCorner',
{appHoverCorner: {elemId: remoteInteraction[uniqueID].hoverCornerItem.id, flag: false}, date: ts});
}
remoteInteraction[uniqueID].setHoverCornerItem(null);
}
}
function moveApplicationWindow(uniqueID, moveApp, portalId) {
var app = SAGE2Items.applications.list[moveApp.elemId];
var titleBarHeight = config.ui.titleBarHeight;
if (portalId !== undefined && portalId !== null) {
titleBarHeight = remoteSharingSessions[portalId].portal.titleBarHeight;
}
var im = findInteractableManager(moveApp.elemId);
if (im) {
drawingManager.applicationMoved(moveApp.elemId, moveApp.elemLeft, moveApp.elemTop);
im.editGeometry(moveApp.elemId, "applications", "rectangle",
{x: moveApp.elemLeft, y: moveApp.elemTop, w: moveApp.elemWidth, h: moveApp.elemHeight + titleBarHeight});
broadcast('setItemPosition', moveApp);
if (SAGE2Items.renderSync.hasOwnProperty(moveApp.elemId)) {
calculateValidBlocks(app, mediaBlockSize, SAGE2Items.renderSync[app.id]);
if (app.id in SAGE2Items.renderSync && SAGE2Items.renderSync[app.id].newFrameGenerated === false) {
handleNewVideoFrame(app.id);
}
}
if (portalId !== undefined && portalId !== null) {
var ts = Date.now() + remoteSharingSessions[portalId].timeOffset;
remoteSharingSessions[portalId].wsio.emit('updateApplicationPosition',
{appPositionAndSize: moveApp, portalId: portalId, date: ts});
}
var updatedStickyItems = stickyAppHandler.moveItemsStickingToUpdatedItem(app);
for (var idx = 0; idx < updatedStickyItems.length; idx++) {
var stickyItem = updatedStickyItems[idx];
im.editGeometry(stickyItem.elemId, "applications", "rectangle", {
x: stickyItem.elemLeft, y: stickyItem.elemTop,
w: stickyItem.elemWidth, h: stickyItem.elemHeight + config.ui.titleBarHeight
});
broadcast('setItemPosition', updatedStickyItems[idx]);
}
}
}
function moveAndResizeApplicationWindow(resizeApp, portalId) {
// Shift position up and left by one pixel to take border into account
// visible in hide-ui mode
resizeApp.elemLeft = resizeApp.elemLeft - 1;
resizeApp.elemTop = resizeApp.elemTop - 1;
var app = SAGE2Items.applications.list[resizeApp.elemId];
var titleBarHeight = config.ui.titleBarHeight;
if (portalId !== undefined && portalId !== null) {
titleBarHeight = remoteSharingSessions[portalId].portal.titleBarHeight;
}
var im = findInteractableManager(resizeApp.elemId);
drawingManager.applicationMoved(resizeApp.elemId, resizeApp.elemLeft, resizeApp.elemTop);
drawingManager.applicationResized(resizeApp.elemId, resizeApp.elemWidth, resizeApp.elemHeight + titleBarHeight,
{x: resizeApp.elemLeft, y: resizeApp.elemTop});
im.editGeometry(resizeApp.elemId, "applications", "rectangle",
{x: resizeApp.elemLeft, y: resizeApp.elemTop, w: resizeApp.elemWidth, h: resizeApp.elemHeight + titleBarHeight});
handleApplicationResize(resizeApp.elemId);
broadcast('setItemPositionAndSize', resizeApp);
if (SAGE2Items.renderSync.hasOwnProperty(resizeApp.elemId)) {
calculateValidBlocks(app, mediaBlockSize, SAGE2Items.renderSync[app.id]);
if (app.id in SAGE2Items.renderSync && SAGE2Items.renderSync[app.id].newFrameGenerated === false) {
handleNewVideoFrame(app.id);
}
}
if (portalId !== undefined && portalId !== null) {
var ts = Date.now() + remoteSharingSessions[portalId].timeOffset;
remoteSharingSessions[portalId].wsio.emit('updateApplicationPositionAndSize',
{appPositionAndSize: resizeApp, portalId: portalId, date: ts});
}
var updatedStickyItems = stickyAppHandler.moveItemsStickingToUpdatedItem(app);
for (var idx = 0; idx < updatedStickyItems.length; idx++) {
var stickyItem = updatedStickyItems[idx];
im.editGeometry(stickyItem.elemId, "applications", "rectangle", {
x: stickyItem.elemLeft, y: stickyItem.elemTop,
w: stickyItem.elemWidth, h: stickyItem.elemHeight + config.ui.titleBarHeight
});
broadcast('setItemPosition', updatedStickyItems[idx]);
}
}
function moveAndResizePartitionWindow(uniqueID, movePartition) {
if (partitions.list.hasOwnProperty(movePartition.elemId)) {
var movedPtn = partitions.list[movePartition.elemId];
// if it is a snapping partition, update all of the neighbors as well
if (movedPtn.isSnapping) {
// then update the neighboring partition positions
var updatedNeighbors = movedPtn.updateNeighborPtnPositions();
// update geometries/display/layout of any updated neighbors
for (var neigh of updatedNeighbors) {
partitions.updatePartitionGeometries(neigh, interactMgr);
broadcast('partitionMoveAndResizeFinished', partitions.list[neigh].getDisplayInfo());
updatePartitionInnerLayout(partitions.list[neigh], true);
}
}
partitions.updatePartitionGeometries(movePartition.elemId, interactMgr);
broadcast('partitionMoveAndResizeFinished', movedPtn.getDisplayInfo());
updatePartitionInnerLayout(movedPtn, true);
}
}
function updatePartitionInnerLayout(partition, animateAppMovement) {
partition.updateInnerLayout();
// update children of partition
let updatedChildren = partition.updateChildrenPositions();
for (let child of updatedChildren) {
child.elemAnimate = animateAppMovement;
moveAndResizeApplicationWindow(child);
}
for (let child of updatedChildren) {
handleStickyItem(child.elemId);
}
}
function moveDataSharingPortalWindow(movePortal) {
interactMgr.editGeometry(movePortal.elemId, "portals", "rectangle", {
x: movePortal.elemLeft, y: movePortal.elemTop,
w: movePortal.elemWidth, h: movePortal.elemHeight + config.ui.titleBarHeight
});
broadcast('setItemPosition', movePortal);
}
function moveAndResizeDataSharingPortalWindow(resizePortal) {
interactMgr.editGeometry(resizePortal.elemId, "portals", "rectangle",
{x: resizePortal.elemLeft, y: resizePortal.elemTop,
w: resizePortal.elemWidth, h: resizePortal.elemHeight + config.ui.titleBarHeight});
handleDataSharingPortalResize(resizePortal.elemId);
broadcast('setItemPositionAndSize', resizePortal);
}
function moveWidgetControls(uniqueID, moveControl) {
var app = SAGE2Items.applications.list[moveControl.appId];
if (app) {
moveControl.appData = getAppPositionSize(app);
broadcast('setControlPosition', moveControl);
var radialGeometry = {
x: moveControl.elemLeft + (moveControl.elemHeight / 2),
y: moveControl.elemTop + (moveControl.elemHeight / 2),
r: moveControl.elemHeight / 2
};
var barGeometry = {
x: moveControl.elemLeft + moveControl.elemHeight,
y: moveControl.elemTop + (moveControl.elemHeight / 2) - (moveControl.elemBarHeight / 2),
w: moveControl.elemWidth - moveControl.elemHeight, h: moveControl.elemBarHeight
};
if (moveControl.hasSideBar === true) {
var shapeData = {
radial: {
type: "circle",
visible: true,
geometry: radialGeometry
},
sidebar: {
type: "rectangle",
visible: true,
geometry: barGeometry
}
};
interactMgr.editComplexGeometry(moveControl.elemId, "widgets", shapeData);
} else {
interactMgr.editGeometry(moveControl.elemId, "widgets", "circle", radialGeometry);
}
/*interactMgr.editGeometry(moveControl.elemId+"_radial", "widgets", "circle", circle);
if(moveControl.hasSideBar === true) {
interactMgr.editGeometry(moveControl.elemId+"_sidebar", "widgets", "rectangle", bar );
}*/
}
}
function sendPointerMoveToApplication(uniqueID, app, pointerX, pointerY, data) {
var ePosition = {x: pointerX - app.left, y: pointerY - (app.top + config.ui.titleBarHeight)};
var eUser = {id: sagePointers[uniqueID].id, label: sagePointers[uniqueID].label, color: sagePointers[uniqueID].color};
var event = {
id: app.id,
type: "pointerMove",
position: ePosition,
user: eUser,
data: data,
date: Date.now()
};
broadcast('eventInItem', event);
}
function pointerRelease(uniqueID, pointerX, pointerY, data) {
if (sagePointers[uniqueID] === undefined) {
return;
}
removeExistingHoverCorner(uniqueID);
// Whiteboard app
if (drawingManager.drawingMode) {
var color = sagePointers[uniqueID] ? sagePointers[uniqueID].color : null;
drawingManager.pointerEvent(
omicronManager.sageToOmicronEvent(uniqueID, pointerX, pointerY, data, 6, color),
uniqueID, pointerX, pointerY, 10, 10);
}
// If obj is undefined (as in this case, will search for radial menu using uniqueID
pointerReleaseOnRadialMenu(uniqueID, pointerX, pointerY, data);
if (remoteInteraction[uniqueID].lockedControl() !== null) {
releaseSlider(uniqueID);
}
var prevInteractionItem = remoteInteraction[uniqueID].releaseOnItem();
if (prevInteractionItem) {
showOrHideWidgetLinks({uniqueID: uniqueID, item: prevInteractionItem, show: false});
}
var obj;
var selectedApp = remoteInteraction[uniqueID].selectedMoveItem || remoteInteraction[uniqueID].selectedResizeItem;
var portal = {id: null};
if (selectedApp !== undefined && selectedApp !== null) {
obj = interactMgr.searchGeometry({x: pointerX, y: pointerY}, null, [selectedApp.id]);
portal = findApplicationPortal(selectedApp) || {id: null};
} else {
obj = interactMgr.searchGeometry({x: pointerX, y: pointerY});
}
if (draggingPartition[uniqueID] && data.button === "left") {
draggingPartition[uniqueID].ptn.left =
pointerX < draggingPartition[uniqueID].start.x ?
pointerX : draggingPartition[uniqueID].start.x;
draggingPartition[uniqueID].ptn.top =
pointerY < draggingPartition[uniqueID].start.y ?
pointerY : draggingPartition[uniqueID].start.y;
draggingPartition[uniqueID].ptn.width =
pointerX < draggingPartition[uniqueID].start.x ?
draggingPartition[uniqueID].start.x - pointerX : pointerX - draggingPartition[uniqueID].start.x;
draggingPartition[uniqueID].ptn.height =
pointerY < draggingPartition[uniqueID].start.y ?
draggingPartition[uniqueID].start.y - pointerY : pointerY - draggingPartition[uniqueID].start.y;
// if the partition is much too small (most likely created by mistake)
if (draggingPartition[uniqueID].ptn.width < partitions.minSize.width
|| draggingPartition[uniqueID].ptn.height < partitions.minSize.height) {
// delete the partition
deletePartition(draggingPartition[uniqueID].ptn.id);
} else {
// increase partition width to minimum width if too thin
if (draggingPartition[uniqueID].ptn.width < partitions.minSize.width) {
draggingPartition[uniqueID].ptn.width = partitions.minSize.width;
}
// increase partition height to minimum height if too short
if (draggingPartition[uniqueID].ptn.height < partitions.minSize.height) {
draggingPartition[uniqueID].ptn.height = partitions.minSize.height;
}
draggingPartition[uniqueID].ptn.aspect =
draggingPartition[uniqueID].ptn.width / draggingPartition[uniqueID].ptn.height;
partitions.updatePartitionGeometries(draggingPartition[uniqueID].ptn.id, interactMgr);
broadcast('partitionMoveAndResizeFinished', draggingPartition[uniqueID].ptn.getDisplayInfo());
broadcast('partitionWindowTitleUpdate', draggingPartition[uniqueID].ptn.getTitle());
// make dragged partition grab content which it is under
partitionsGrabAllContent();
}
// stop creation of partition
delete draggingPartition[uniqueID];
return;
}
if (cuttingPartition[uniqueID] && data.button === "left") {
cuttingPartition[uniqueID].end = {x: pointerX, y: pointerY};
var cutDirection = +(pointerX - cuttingPartition[uniqueID].start.x) >
+(pointerY - cuttingPartition[uniqueID].start.y) ?
"horizontal" : "vertical";
var oldPtn = cuttingPartition[uniqueID].ptn;
var newPtn1 = cuttingPartition[uniqueID].newPtn1;
var newPtn2 = cuttingPartition[uniqueID].newPtn2;
// if mouse is dragged outside of old partition, consider that to be a cancelled split, delete the new partitions
// also do nothing if partition is not large enough to be split
if (pointerX < oldPtn.left || pointerX > oldPtn.left + oldPtn.width ||
pointerY < oldPtn.top || pointerY > oldPtn.top + oldPtn.height ||
(cutDirection === "horizontal" && oldPtn.height < partitions.minSize.height * 2) ||
(cutDirection === "vertical" && oldPtn.width < partitions.minSize.width * 2)) {
// cancel operation, delete new partitions
if (newPtn1) {
deletePartition(newPtn1.id);
}
if (newPtn2) {
deletePartition(newPtn2.id);
}
} else {
// otherwise, delete old partition, assign items to new partitions
// make sure that the new partitions exist
// it is possible that they wouldn't if the drag gesture was too small
// if they don't exist, do nothing
if (newPtn1 && newPtn2) {
var cutPtnItems = Object.assign({}, oldPtn.children);
// var ptnColor = oldPtn.color;
var ptnTiled = oldPtn.innerTiling; // to preserve tiling of new partitions
var ptnSnapping = oldPtn.isSnapping;
// delete the old partition
deletePartition(oldPtn.id);
// reassign content from oldPtn to the 2 new partitions
for (var key in cutPtnItems) {
partitions.updateOnItemRelease(cutPtnItems[key]);
}
newPtn1.isSnapping = ptnSnapping;
newPtn2.isSnapping = ptnSnapping;
if (ptnSnapping) {
partitions.updateNeighbors(newPtn1.id);
partitions.updateNeighbors(newPtn2.id);
// after calculating neighbors, update display
broadcast('updatePartitionSnapping', newPtn1.getDisplayInfo());
for (let p of Object.keys(newPtn1.neighbors)) {
if (partitions.list[p]) {
broadcast('updatePartitionSnapping', partitions.list[p].getDisplayInfo());
}
}
// after calculating neighbors, update display
broadcast('updatePartitionSnapping', newPtn2.getDisplayInfo());
for (let p of Object.keys(newPtn2.neighbors)) {
if (partitions.list[p]) {
broadcast('updatePartitionSnapping', partitions.list[p].getDisplayInfo());
}
}
}
// if the old partition was tiled, set the new displays to be tiled
if (ptnTiled) {
newPtn1.toggleInnerTiling();
updatePartitionInnerLayout(newPtn1, true);
newPtn2.toggleInnerTiling();
updatePartitionInnerLayout(newPtn2, true);
}
// update parititon titles
broadcast('partitionWindowTitleUpdate', newPtn1.getTitle());
broadcast('partitionWindowTitleUpdate', newPtn2.getTitle());
// return borders to normal
broadcast('updatePartitionBorders', {id: newPtn1.id, highlight: false});
broadcast('updatePartitionBorders', {id: newPtn2.id, highlight: false});
}
}
// stop division of partition
delete cuttingPartition[uniqueID];
return;
}
// update parent partition of item when the app is released
if (selectedApp && selectedApp.id && SAGE2Items.applications.list.hasOwnProperty(selectedApp.id)) {
var changedPartitions = partitions.updateOnItemRelease(selectedApp, {x: pointerX, y: pointerY});
moveAndResizeApplicationWindow({
elemId: selectedApp.id, elemLeft: selectedApp.left,
elemTop: selectedApp.top, elemWidth: selectedApp.width,
elemHeight: selectedApp.height, date: new Date()
});
changedPartitions.forEach(el => {
broadcast('partitionWindowTitleUpdate', partitions.list[el].getTitle());
updatePartitionInnerLayout(partitions.list[el], true);
});
// remove partition edge highlight
broadcast('updatePartitionBorders', null);
}
if (obj === null) {
dropSelectedItem(uniqueID, true, portal.id);
return;
}
var localPt = globalToLocal(pointerX, pointerY, obj.type, obj.geometry);
switch (obj.layerId) {
case "staticUI": {
if (portal.id !== null) {
dropSelectedItem(uniqueID, true, portal.id);
}
pointerReleaseOnStaticUI(uniqueID, pointerX, pointerY, obj, portal.id);
break;
}
case "radialMenus": {
pointerReleaseOnRadialMenu(uniqueID, pointerX, pointerY, data, obj);
dropSelectedItem(uniqueID, true, portal.id);
break;
}
case "applications": {
if (dropSelectedItem(uniqueID, true, portal.id) === null) {
if (remoteInteraction[uniqueID].appInteractionMode()) {
sendPointerReleaseToApplication(uniqueID, obj.data, pointerX, pointerY, data);
}
}
break;
}
case "partitions": {
// pointer release on partition (no functionality yet)
dropSelectedItem(uniqueID, true, portal.id);
break;
}
case "portals": {
pointerReleaseOnPortal(uniqueID, obj.data.id, localPt, data);
break;
}
case "widgets": {
pointerPressOrReleaseOnWidget(uniqueID, pointerX, pointerY, data, obj, localPt, "release");
dropSelectedItem(uniqueID, true, portal.id);
break;
}
default: {
dropSelectedItem(uniqueID, true, portal.id);
}
}
}
function pointerReleaseOnStaticUI(uniqueID, pointerX, pointerY, obj) {
// don't allow data-pushing
// dropSelectedItem(uniqueID, true);
/*
var remote = obj.data;
var app = dropSelectedItem(uniqueID, false, null);
if (app !== null && SAGE2Items.applications.list.hasOwnProperty(app.application.id) && remote.connected) {
remote.wsio.emit('addNewElementFromRemoteServer', app.application);
var eLogData = {
host: remote.wsio.remoteAddress.address,
port: remote.wsio.remoteAddress.port,
application: {
id: app.application.id,
type: app.application.application
}
};
addEventToUserLog(uniqueID, {type: "shareApplication", data: eLogData, time: Date.now()});
}
*/
var remote = obj.data;
var app = dropSelectedItem(uniqueID, false, null);
if (app !== null && SAGE2Items.applications.list.hasOwnProperty(app.application.id) && remote.connected === "on") {
shareApplicationWithRemoteSite(uniqueID, app, remote);
}
}
/**
* Shares an application with a remote site
*
* @method shareApplicationWithRemoteSite
* @param {String} uniqueID - The wsio.id.
* @param {Object} app - The app object to share. Usually: {application: SAGE2Items.applications.list[data.app]}
* @param {Object} remote - Remote site. Usually: remoteSites[data.parameters.remoteSiteIndex]
*/
function shareApplicationWithRemoteSite(uniqueID, app, remote) {
var sharedId = app.application.id + "_" + config.host + ":" + config.secure_port + "+" + remote.wsio.id;
if (sharedApps[app.application.id] === undefined) {
sharedApps[app.application.id] = [{wsio: remote.wsio, sharedId: sharedId}];
} else {
sharedApps[app.application.id].push({wsio: remote.wsio, sharedId: sharedId});
}
SAGE2Items.applications.editButtonVisibilityOnItem(app.application.id, "syncButton", true);
remote.wsio.emit('addNewSharedElementFromRemoteServer',
{application: app.application, id: sharedId, remoteAppId: app.application.id});
broadcast('setAppSharingFlag', {id: app.application.id, sharing: true});
var eLogData = {
host: remote.wsio.remoteAddress.address,
port: remote.wsio.remoteAddress.port,
application: {
id: app.application.id,
type: app.application.application
}
};
addEventToUserLog(uniqueID, {type: "shareApplication", data: eLogData, time: Date.now()});
}
function pointerReleaseOnPortal(uniqueID, portalId, localPt, data) {
var obj = interactMgr.getObject(portalId, "portals");
var selectedApp = remoteInteraction[uniqueID].selectedMoveItem || remoteInteraction[uniqueID].selectedResizeItem;
if (selectedApp) {
var portal = findApplicationPortal(selectedApp);
if (portal !== undefined && portal !== null && portal.id === portalId) {
dropSelectedItem(uniqueID, true, portalId);
return;
}
var app = dropSelectedItem(uniqueID, false, null);
localPt = globalToLocal(app.previousPosition.left, app.previousPosition.top, obj.type, obj.geometry);
var remote = remoteSharingSessions[obj.id];
createAppFromDescription(app.application, function(appInstance, videohandle) {
if (appInstance.application === "media_stream" || appInstance.application === "media_block_stream") {
appInstance.id = app.application.id + "_" + obj.data.id;
} else {
appInstance.id = getUniqueSharedAppId(obj.data.id);
}
appInstance.left = localPt.x / obj.data.scale;
appInstance.top = (localPt.y - config.ui.titleBarHeight) / obj.data.scale;
appInstance.width = app.previousPosition.width / obj.data.scale;
appInstance.height = app.previousPosition.height / obj.data.scale;
remoteSharingSessions[obj.data.id].appCount++;
// if (SAGE2Items.renderSync.hasOwnProperty(app.id) {
var i;
SAGE2Items.renderSync[appInstance.id] = {clients: {}, date: Date.now()};
for (i = 0; i < clients.length; i++) {
if (clients[i].clientType === "display") {
SAGE2Items.renderSync[appInstance.id].clients[clients[i].id] = {
wsio: clients[i], readyForNextFrame: false, blocklist: []
};
}
}
handleNewApplicationInDataSharingPortal(appInstance, videohandle, obj.data.id);
remote.wsio.emit('addNewRemoteElementInDataSharingPortal', appInstance);
var eLogData = {
host: remote.portal.host,
port: remote.portal.port,
application: {
id: appInstance.id,
type: appInstance.application
}
};
addEventToUserLog(uniqueID, {type: "shareApplication", data: eLogData, time: Date.now()});
});
} else {
if (remoteInteraction[uniqueID].appInteractionMode()) {
var scaledPt = {x: localPt.x / obj.data.scale, y: (localPt.y - config.ui.titleBarHeight) / obj.data.scale};
var pObj = SAGE2Items.portals.interactMgr[portalId].searchGeometry(scaledPt);
if (pObj === null) {
return;
}
// var pLocalPt = globalToLocal(scaledPt.x, scaledPt.y, pObj.type, pObj.geometry);
switch (pObj.layerId) {
case "radialMenus": {
break;
}
case "widgets": {
break;
}
case "applications": {
sendPointerReleaseToApplication(uniqueID, pObj.data, scaledPt.x, scaledPt.y, data);
break;
}
}
}
}
}
function pointerReleaseOnRadialMenu(uniqueID, pointerX, pointerY, data, obj) {
if (obj === undefined) {
for (var key in SAGE2Items.radialMenus.list) {
radialMenu = SAGE2Items.radialMenus.list[key];
// console.log(data.id+"_menu: " + radialMenu);
if (radialMenu !== undefined) {
radialMenu.onRelease(uniqueID);
}
}
// If pointer release is outside window, use the pointerRelease type to end the
// scroll event, but don't trigger any clicks because of a 'null' button (clicks expects a left/right)
data = { button: "null", color: sagePointers[uniqueID].color };
radialMenuEvent({type: "pointerRelease", id: uniqueID, x: pointerX, y: pointerY, data: data});
} else {
var radialMenu = obj.data;
if (obj.id.indexOf("menu_radial_button") !== -1) {
// Pressing on radial menu button
// console.log("pointer release on radial button: " + obj.id);
radialMenu.onRelease(uniqueID);
var menuState = radialMenu.onButtonEvent(obj.id, uniqueID, "pointerRelease");
if (menuState !== undefined) {
radialMenuEvent({type: "stateChange", menuID: radialMenu.id, menuState: menuState });
}
} else if (obj.id.indexOf("menu_thumbnail") !== -1) {
// PointerRelease on thumbnail window
// console.log("Pointer release on thumbnail window");
data = { button: data.button, color: sagePointers[uniqueID].color };
radialMenuEvent({type: "pointerRelease", id: uniqueID, x: pointerX, y: pointerY, data: data});
} else {
// Not on a button
radialMenu = obj.data.onRelease(uniqueID);
}
}
}
function dropSelectedItem(uniqueID, valid, portalId) {
var item;
var position;
if (remoteInteraction[uniqueID].selectedMoveItem !== null) {
// check which list contains the move item selected
if (SAGE2Items.portals.list.hasOwnProperty(remoteInteraction[uniqueID].selectedMoveItem.id)) {
// if the item is a portal
item = SAGE2Items.portals.list[remoteInteraction[uniqueID].selectedMoveItem.id];
} else if (partitions.list.hasOwnProperty(remoteInteraction[uniqueID].selectedMoveItem.id)) {
// if the item is a partition
item = partitions.list[remoteInteraction[uniqueID].selectedMoveItem.id];
} else {
item = SAGE2Items.applications.list[remoteInteraction[uniqueID].selectedMoveItem.id];
}
if (item) {
position = {left: item.left, top: item.top, width: item.width, height: item.height};
dropMoveItem(uniqueID, item, valid, portalId);
return {application: item, previousPosition: position};
}
} else if (remoteInteraction[uniqueID].selectedResizeItem !== null) {
// check which list contains the item selected
if (SAGE2Items.portals.list.hasOwnProperty(remoteInteraction[uniqueID].selectedResizeItem.id)) {
// if the item is a portal
item = SAGE2Items.portals.list[remoteInteraction[uniqueID].selectedResizeItem.id];
} else if (partitions.list.hasOwnProperty(remoteInteraction[uniqueID].selectedResizeItem.id)) {
// if the item is a partition
item = partitions.list[remoteInteraction[uniqueID].selectedResizeItem.id];
} else {
item = SAGE2Items.applications.list[remoteInteraction[uniqueID].selectedResizeItem.id];
}
if (item) {
position = {left: item.left, top: item.top, width: item.width, height: item.height};
dropResizeItem(uniqueID, item, portalId);
return {application: item, previousPosition: position};
}
}
return null;
}
function dropMoveItem(uniqueID, app, valid, portalId) {
if (valid !== false) {
valid = true;
}
var updatedItem = remoteInteraction[uniqueID].releaseItem(valid);
if (updatedItem !== null) {
moveApplicationWindow(uniqueID, updatedItem, portalId);
}
handleStickyItem(app.id);
broadcast('finishedMove', {id: app.id, date: Date.now()});
if (portalId !== undefined && portalId !== null) {
var ts = Date.now() + remoteSharingSessions[portalId].timeOffset;
remoteSharingSessions[portalId].wsio.emit('finishApplicationMove', {id: uniqueID, appId: app.id, date: ts});
}
var eLogData = {
type: "move",
action: "end",
application: {
id: app.id,
type: app.application
},
location: {
x: parseInt(app.left, 10),
y: parseInt(app.top, 10),
width: parseInt(app.width, 10),
height: parseInt(app.height, 10)
}
};
addEventToUserLog(uniqueID, {type: "windowManagement", data: eLogData, time: Date.now()});
}
function dropResizeItem(uniqueID, app, portalId) {
remoteInteraction[uniqueID].releaseItem(true);
broadcast('finishedResize', {id: app.id, date: Date.now()});
if (portalId !== undefined && portalId !== null) {
var ts = Date.now() + remoteSharingSessions[portalId].timeOffset;
remoteSharingSessions[portalId].wsio.emit('finishApplicationResize', {id: uniqueID, appId: app.id, date: ts});
}
var eLogData = {
type: "resize",
action: "end",
application: {
id: app.id,
type: app.application
},
location: {
x: parseInt(app.left, 10),
y: parseInt(app.top, 10),
width: parseInt(app.width, 10),
height: parseInt(app.height, 10)
}
};
addEventToUserLog(uniqueID, {type: "windowManagement", data: eLogData, time: Date.now()});
}
function sendPointerReleaseToApplication(uniqueID, app, pointerX, pointerY, data) {
var ePosition = {x: pointerX - app.left, y: pointerY - (app.top + config.ui.titleBarHeight)};
var eUser = {id: sagePointers[uniqueID].id, label: sagePointers[uniqueID].label, color: sagePointers[uniqueID].color};
var event = {
id: app.id,
type: "pointerRelease",
position: ePosition,
user: eUser,
data: data,
date: Date.now()
};
broadcast('eventInItem', event);
}
function pointerDblClick(uniqueID, pointerX, pointerY) {
if (sagePointers[uniqueID] === undefined) {
return;
}
var obj = interactMgr.searchGeometry({x: pointerX, y: pointerY});
if (obj === null) {
return;
}
var localPt = globalToLocal(pointerX, pointerY, obj.type, obj.geometry);
switch (obj.layerId) {
case "applications": {
pointerDblClickOnApplication(uniqueID, pointerX, pointerY, obj, localPt);
break;
}
case "portals": {
break;
}
}
}
function pointerDblClickOnApplication(uniqueID, pointerX, pointerY, obj, localPt) {
var btn = SAGE2Items.applications.findButtonByPoint(obj.id, localPt);
// pointer press on app window
if (btn === null) {
if (remoteInteraction[uniqueID].windowManagementMode()) {
toggleApplicationFullscreen(uniqueID, obj.data, true);
} else {
sendPointerDblClickToApplication(uniqueID, obj.data, pointerX, pointerY);
}
return;
}
switch (btn.id) {
case "titleBar": {
toggleApplicationFullscreen(uniqueID, obj.data, true);
break;
}
case "dragCorner": {
break;
}
case "fullscreenButton": {
break;
}
case "pinButton": {
break;
}
case "closeButton": {
break;
}
}
}
function pointerScrollStart(uniqueID, pointerX, pointerY) {
if (sagePointers[uniqueID] === undefined) {
return;
}
var obj = interactMgr.searchGeometry({x: pointerX, y: pointerY});
if (obj === null) {
return;
}
var localPt = globalToLocal(pointerX, pointerY, obj.type, obj.geometry);
switch (obj.layerId) {
case "staticUI": {
break;
}
case "radialMenus": {
break;
}
case "widgets": {
break;
}
case "applications": {
pointerScrollStartOnApplication(uniqueID, pointerX, pointerY, obj, localPt);
break;
}
case "portals": {
break;
}
}
}
function pointerScrollStartOnApplication(uniqueID, pointerX, pointerY, obj, localPt) {
var btn = SAGE2Items.applications.findButtonByPoint(obj.id, localPt);
interactMgr.moveObjectToFront(obj.id, obj.layerId);
var app = SAGE2Items.applications.list[obj.id];
var stickyList = stickyAppHandler.getStickingItems(app);
for (var idx in stickyList) {
interactMgr.moveObjectToFront(stickyList[idx].id, "applications", ["portals"]);
}
var newOrder = interactMgr.getObjectZIndexList("applications", ["portals"]);
broadcast('updateItemOrder', newOrder);
// pointer scroll on app window
if (btn === null) {
if (remoteInteraction[uniqueID].windowManagementMode()) {
selectApplicationForScrollResize(uniqueID, obj.data, pointerX, pointerY);
} else if (remoteInteraction[uniqueID].appInteractionMode()) {
remoteInteraction[uniqueID].selectWheelItem = obj.data;
remoteInteraction[uniqueID].selectWheelDelta = 0;
}
return;
}
switch (btn.id) {
case "titleBar": {
selectApplicationForScrollResize(uniqueID, obj.data, pointerX, pointerY);
break;
}
case "dragCorner": {
if (remoteInteraction[uniqueID].windowManagementMode()) {
selectApplicationForScrollResize(uniqueID, obj.data, pointerX, pointerY);
} else if (remoteInteraction[uniqueID].appInteractionMode()) {
remoteInteraction[uniqueID].selectWheelItem = obj.data;
remoteInteraction[uniqueID].selectWheelDelta = 0;
}
break;
}
case "fullscreenButton": {
selectApplicationForScrollResize(uniqueID, obj.data, pointerX, pointerY);
break;
}
case "pinButton": {
selectApplicationForScrollResize(uniqueID, obj.data, pointerX, pointerY);
break;
}
case "closeButton": {
selectApplicationForScrollResize(uniqueID, obj.data, pointerX, pointerY);
break;
}
}
}
function selectApplicationForScrollResize(uniqueID, app, pointerX, pointerY) {
remoteInteraction[uniqueID].selectScrollItem(app);
broadcast('startMove', {id: app.id, date: Date.now()});
broadcast('startResize', {id: app.id, date: Date.now()});
var a = {
id: app.id,
type: app.application
};
var l = {
x: parseInt(app.left, 10),
y: parseInt(app.top, 10),
width: parseInt(app.width, 10),
height: parseInt(app.height, 10)
};
addEventToUserLog(uniqueID, {type: "windowManagement", data:
{type: "move", action: "start", application: a, location: l}, time: Date.now()});
addEventToUserLog(uniqueID, {type: "windowManagement", data:
{type: "resize", action: "start", application: a, location: l}, time: Date.now()});
}
function pointerScroll(uniqueID, data) {
if (sagePointers[uniqueID] === undefined) {
return;
}
var pointerX = sagePointers[uniqueID].left;
var pointerY = sagePointers[uniqueID].top;
var scale = 1.0 + Math.abs(data.wheelDelta) / 512;
if (data.wheelDelta > 0) {
scale = 1.0 / scale;
}
var updatedResizeItem = remoteInteraction[uniqueID].scrollSelectedItem(scale);
if (updatedResizeItem !== null) {
moveAndResizeApplicationWindow(updatedResizeItem);
} else {
var obj = interactMgr.searchGeometry({x: pointerX, y: pointerY});
if (obj === null) {
return;
}
// var localPt = globalToLocal(pointerX, pointerY, obj.type, obj.geometry);
switch (obj.layerId) {
case "staticUI": {
break;
}
case "radialMenus": {
sendPointerScrollToRadialMenu(uniqueID, obj, pointerX, pointerY, data);
break;
}
case "widgets": {
break;
}
case "applications": {
sendPointerScrollToApplication(uniqueID, obj.data, pointerX, pointerY, data);
break;
}
}
}
}
function sendPointerScrollToRadialMenu(uniqueID, obj, pointerX, pointerY, data) {
if (obj.id.indexOf("menu_thumbnail") !== -1) {
var event = { button: data.button, color: sagePointers[uniqueID].color, wheelDelta: data.wheelDelta };
radialMenuEvent({type: "pointerScroll", id: uniqueID, x: pointerX, y: pointerY, data: event});
}
remoteInteraction[uniqueID].selectWheelDelta += data.wheelDelta;
}
function sendPointerScrollToApplication(uniqueID, app, pointerX, pointerY, data) {
var ePosition = {x: pointerX - app.left, y: pointerY - (app.top + config.ui.titleBarHeight)};
var eUser = {id: sagePointers[uniqueID].id, label: sagePointers[uniqueID].label, color: sagePointers[uniqueID].color};
var event = {id: app.id, type: "pointerScroll", position: ePosition, user: eUser, data: data, date: Date.now()};
broadcast('eventInItem', event);
remoteInteraction[uniqueID].selectWheelDelta += data.wheelDelta;
}
function pointerScrollEnd(uniqueID) {
if (sagePointers[uniqueID] === undefined) {
return;
}
var updatedResizeItem = remoteInteraction[uniqueID].selectedScrollItem;
if (updatedResizeItem !== null) {
broadcast('finishedMove', {id: updatedResizeItem.id, date: Date()});
broadcast('finishedResize', {id: updatedResizeItem.id, date: Date.now()});
var a = {
id: updatedResizeItem.id,
type: updatedResizeItem.application
};
var l = {
x: parseInt(updatedResizeItem.left, 10),
y: parseInt(updatedResizeItem.top, 10),
width: parseInt(updatedResizeItem.width, 10),
height: parseInt(updatedResizeItem.height, 10)
};
addEventToUserLog(uniqueID, {type: "windowManagement",
data: {type: "move", action: "end", application: a, location: l}, time: Date.now()});
addEventToUserLog(uniqueID, {type: "windowManagement",
data: {type: "resize", action: "end", application: a, location: l}, time: Date.now()});
remoteInteraction[uniqueID].selectedScrollItem = null;
} else {
if (remoteInteraction[uniqueID].appInteractionMode()) {
var app = remoteInteraction[uniqueID].selectWheelItem;
if (app !== undefined && app !== null) {
var eLogData = {
type: "pointerScroll",
application: {
id: app.id,
type: app.application
},
wheelDelta: remoteInteraction[uniqueID].selectWheelDelta
};
addEventToUserLog(uniqueID, {type: "applicationInteraction", data: eLogData, time: Date.now()});
}
}
}
}
function checkForSpecialKeys(uniqueID, code, flag) {
switch (code) {
case 16: {
remoteInteraction[uniqueID].SHIFT = flag;
break;
}
case 17: {
remoteInteraction[uniqueID].CTRL = flag;
break;
}
case 18: {
remoteInteraction[uniqueID].ALT = flag;
break;
}
case 20: {
remoteInteraction[uniqueID].CAPS = flag;
break;
}
case 91:
case 92:
case 93: {
remoteInteraction[uniqueID].CMD = flag;
break;
}
}
}
function keyDown(uniqueID, pointerX, pointerY, data) {
if (sagePointers[uniqueID] === undefined) {
return;
}
checkForSpecialKeys(uniqueID, data.code, true);
// if (remoteInteraction[uniqueID].appInteractionMode()) {
// luc: send keys to app anyway
var obj = interactMgr.searchGeometry({x: pointerX, y: pointerY});
if (obj === null) {
return;
}
var localPt = globalToLocal(pointerX, pointerY, obj.type, obj.geometry);
switch (obj.layerId) {
case "staticUI": {
break;
}
case "radialMenus": {
break;
}
case "widgets": {
break;
}
case "applications": {
sendKeyDownToApplication(uniqueID, obj.data, localPt, data);
break;
}
case "portals": {
keyDownOnPortal(uniqueID, obj.data.id, localPt, data);
break;
}
}
// }
}
function sendKeyDownToApplication(uniqueID, app, localPt, data) {
var portal = findApplicationPortal(app);
var titleBarHeight = config.ui.titleBarHeight;
if (portal !== undefined && portal !== null) {
titleBarHeight = portal.data.titleBarHeight;
}
var ePosition = {x: localPt.x, y: localPt.y - titleBarHeight};
var eUser = {id: sagePointers[uniqueID].id, label: sagePointers[uniqueID].label, color: sagePointers[uniqueID].color};
var eData = {
code: data.code,
state: "down",
// add also the state of the special keys
status: {
SHIFT: remoteInteraction[uniqueID].SHIFT,
CTRL: remoteInteraction[uniqueID].CTRL,
ALT: remoteInteraction[uniqueID].ALT,
CAPS: remoteInteraction[uniqueID].CAPS,
CMD: remoteInteraction[uniqueID].CMD
}
};
if (fileBufferManager.hasFileBufferForApp(app.id)) {
eData.bufferUpdate = fileBufferManager.insertChar({appId: app.id, code: data.code,
printable: false, user_id: sagePointers[uniqueID].id});
}
var event = {id: app.id, type: "specialKey", position: ePosition, user: eUser, data: eData, date: Date.now()};
broadcast('eventInItem', event);
var eLogData = {
type: "specialKey",
application: {
id: app.id,
type: app.application
},
code: eData.code,
state: eData.state
};
addEventToUserLog(uniqueID, {type: "applicationInteraction", data: eLogData, time: Date.now()});
}
function keyDownOnPortal(uniqueID, portalId, localPt, data) {
checkForSpecialKeys(uniqueID, data.code, true);
var portal = SAGE2Items.portals.list[portalId];
var scaledPt = {x: localPt.x / portal.scale, y: (localPt.y - config.ui.titleBarHeight) / portal.scale};
if (remoteInteraction[uniqueID].local && remoteInteraction[uniqueID].portal !== null) {
var rData = {
id: uniqueID,
left: scaledPt.x,
top: scaledPt.y,
code: data.code
};
remoteSharingSessions[portalId].wsio.emit('remoteSageKeyDown', rData);
}
var pObj = SAGE2Items.portals.interactMgr[portalId].searchGeometry(scaledPt);
if (pObj === null) {
return;
}
// var pLocalPt = globalToLocal(scaledPt.x, scaledPt.y, pObj.type, pObj.geometry);
switch (pObj.layerId) {
case "radialMenus": {
break;
}
case "widgets": {
break;
}
case "applications": {
sendKeyDownToApplication(uniqueID, pObj.data, scaledPt, data);
break;
}
}
}
function keyUp(uniqueID, pointerX, pointerY, data) {
if (sagePointers[uniqueID] === undefined) {
return;
}
checkForSpecialKeys(uniqueID, data.code, false);
if (remoteInteraction[uniqueID].modeChange !== undefined && (data.code === 9 || data.code === 16)) {
return;
}
var lockedControl = remoteInteraction[uniqueID].lockedControl();
if (lockedControl !== null) {
var eUser = {id: sagePointers[uniqueID].id, label: sagePointers[uniqueID].label,
color: sagePointers[uniqueID].color};
var event = {code: data.code, printable: false, state: "up", ctrlId: lockedControl.ctrlId,
appId: lockedControl.appId, instanceID: lockedControl.instanceID, user: eUser,
date: Date.now()};
broadcast('keyInTextInputWidget', event);
if (data.code === 13) {
// Enter key
remoteInteraction[uniqueID].dropControl();
}
return;
}
var obj = interactMgr.searchGeometry({x: pointerX, y: pointerY});
if (obj === null) {
return;
}
var localPt = globalToLocal(pointerX, pointerY, obj.type, obj.geometry);
switch (obj.layerId) {
case "staticUI": {
break;
}
case "radialMenus": {
break;
}
case "widgets": {
break;
}
case "applications": {
if (remoteInteraction[uniqueID].windowManagementMode() &&
(data.code === 8 || data.code === 46)) {
// backspace or delete
deleteApplication(obj.data.id, null, {id: uniqueID});
var eLogData = {
application: {
id: obj.data.id,
type: obj.data.application
}
};
addEventToUserLog(uniqueID, {type: "delete", data: eLogData, time: Date.now()});
// } else {
// sendKeyUpToApplication(uniqueID, obj.data, localPt, data);
// }
}
// luc: send keys to app anyway
sendKeyUpToApplication(uniqueID, obj.data, localPt, data);
break;
}
case "portals": {
keyUpOnPortal(uniqueID, obj.data.id, localPt, data);
break;
}
}
}
function sendKeyUpToApplication(uniqueID, app, localPt, data) {
var portal = findApplicationPortal(app);
var titleBarHeight = config.ui.titleBarHeight;
if (portal !== undefined && portal !== null) {
titleBarHeight = portal.data.titleBarHeight;
}
var ePosition = {x: localPt.x, y: localPt.y - titleBarHeight};
var eUser = {id: sagePointers[uniqueID].id, label: sagePointers[uniqueID].label, color: sagePointers[uniqueID].color};
var eData = {code: data.code, state: "up"};
var event = {id: app.id, type: "specialKey", position: ePosition, user: eUser, data: eData, date: Date.now()};
broadcast('eventInItem', event);
var eLogData = {
type: "specialKey",
application: {
id: app.id,
type: app.application
},
code: eData.code,
state: eData.state
};
addEventToUserLog(uniqueID, {type: "applicationInteraction", data: eLogData, time: Date.now()});
}
function keyUpOnPortal(uniqueID, portalId, localPt, data) {
checkForSpecialKeys(uniqueID, data.code, false);
var portal = SAGE2Items.portals.list[portalId];
var scaledPt = {x: localPt.x / portal.scale, y: (localPt.y - config.ui.titleBarHeight) / portal.scale};
if (remoteInteraction[uniqueID].local && remoteInteraction[uniqueID].portal !== null) {
var rData = {
id: uniqueID,
left: scaledPt.x,
top: scaledPt.y,
code: data.code
};
remoteSharingSessions[portalId].wsio.emit('remoteSageKeyUp', rData);
}
var pObj = SAGE2Items.portals.interactMgr[portalId].searchGeometry(scaledPt);
if (pObj === null) {
return;
}
// var pLocalPt = globalToLocal(scaledPt.x, scaledPt.y, pObj.type, pObj.geometry);
switch (pObj.layerId) {
case "radialMenus": {
break;
}
case "widgets": {
break;
}
case "applications": {
sendKeyUpToApplication(uniqueID, pObj.data, scaledPt, data);
break;
}
}
}
function keyPress(uniqueID, pointerX, pointerY, data) {
if (sagePointers[uniqueID] === undefined) {
return;
}
var modeSwitch = false;
if (data.code === 9 && remoteInteraction[uniqueID].SHIFT && sagePointers[uniqueID].visible) {
// shift + tab
remoteInteraction[uniqueID].toggleModes();
broadcast('changeSagePointerMode', {id: sagePointers[uniqueID].id, mode: remoteInteraction[uniqueID].interactionMode});
if (remoteInteraction[uniqueID].modeChange !== undefined) {
clearTimeout(remoteInteraction[uniqueID].modeChange);
}
remoteInteraction[uniqueID].modeChange = setTimeout(function() {
delete remoteInteraction[uniqueID].modeChange;
}, 500);
modeSwitch = true;
}
var lockedControl = remoteInteraction[uniqueID].lockedControl();
if (lockedControl !== null) {
var eUser = {
id: sagePointers[uniqueID].id, label: sagePointers[uniqueID].label,
color: sagePointers[uniqueID].color
};
var event = {
code: data.code, printable: true, state: "press", ctrlId: lockedControl.ctrlId,
appId: lockedControl.appId, instanceID: lockedControl.instanceID, user: eUser,
date: Date.now()
};
broadcast('keyInTextInputWidget', event);
if (data.code === 13) {
// Enter key
remoteInteraction[uniqueID].dropControl();
}
return;
}
var obj = interactMgr.searchGeometry({x: pointerX, y: pointerY});
if (obj === null) {
// if in empty space:
// Pressing ? for help (with shift)
if (data.code === 63 && remoteInteraction[uniqueID].SHIFT) {
// Load the cheet sheet on the wall
wsLoadApplication({id: "127.0.0.1:42"}, {
application: "/uploads/pdfs/cheat-sheet.pdf",
user: "127.0.0.1:42",
// position in center and 100pix down
position: [0.5, 100]
});
// show a popup
// broadcast('toggleHelp', {});
}
return;
}
var localPt = globalToLocal(pointerX, pointerY, obj.type, obj.geometry);
switch (obj.layerId) {
case "staticUI": {
break;
}
case "radialMenus": {
break;
}
case "widgets": {
break;
}
case "applications": {
// if (modeSwitch === false && remoteInteraction[uniqueID].appInteractionMode()) {
// luc: send keys to app anyway
if (modeSwitch === false) {
sendKeyPressToApplication(uniqueID, obj.data, localPt, data);
}
break;
}
case "portals": {
if (modeSwitch === true) {
remoteSharingSessions[obj.data.id].wsio.emit('remoteSagePointerToggleModes',
{id: uniqueID, mode: remoteInteraction[uniqueID].interactionMode});
} else if (remoteInteraction[uniqueID].appInteractionMode()) {
keyPressOnPortal(uniqueID, obj.data.id, localPt, data);
}
break;
}
}
}
function sendKeyPressToApplication(uniqueID, app, localPt, data) {
var portal = findApplicationPortal(app);
var titleBarHeight = config.ui.titleBarHeight;
if (portal !== undefined && portal !== null) {
titleBarHeight = portal.data.titleBarHeight;
}
var ePosition = {x: localPt.x, y: localPt.y - titleBarHeight};
var eUser = {id: sagePointers[uniqueID].id, label: sagePointers[uniqueID].label, color: sagePointers[uniqueID].color};
if (fileBufferManager.hasFileBufferForApp(app.id)) {
data.bufferUpdate = fileBufferManager.insertChar({appId: app.id, code: data.code,
printable: true, user_id: sagePointers[uniqueID].id});
}
var event = {id: app.id, type: "keyboard", position: ePosition, user: eUser, data: data, date: Date.now()};
broadcast('eventInItem', event);
var eLogData = {
type: "keyboard",
application: {
id: app.id,
type: app.application
},
code: data.code,
character: data.character
};
addEventToUserLog(uniqueID, {type: "applicationInteraction", data: eLogData, time: Date.now()});
}
function keyPressOnPortal(uniqueID, portalId, localPt, data) {
var portal = SAGE2Items.portals.list[portalId];
var scaledPt = {x: localPt.x / portal.scale, y: (localPt.y - config.ui.titleBarHeight) / portal.scale};
if (remoteInteraction[uniqueID].local && remoteInteraction[uniqueID].portal !== null) {
var rData = {
id: uniqueID,
left: scaledPt.x,
top: scaledPt.y,
code: data.code,
character: data.character
};
remoteSharingSessions[portalId].wsio.emit('remoteSageKeyPress', rData);
}
var pObj = SAGE2Items.portals.interactMgr[portalId].searchGeometry(scaledPt);
if (pObj === null) {
return;
}
// var pLocalPt = globalToLocal(scaledPt.x, scaledPt.y, pObj.type, pObj.geometry);
switch (pObj.layerId) {
case "radialMenus": {
break;
}
case "widgets": {
break;
}
case "applications": {
sendKeyPressToApplication(uniqueID, pObj.data, scaledPt, data);
break;
}
}
}
function toggleApplicationFullscreen(uniqueID, app, dblClick) {
var resizeApp;
if (app.maximized !== true) { // maximize
resizeApp = remoteInteraction[uniqueID].maximizeSelectedItem(app);
} else { // restore to previous
resizeApp = remoteInteraction[uniqueID].restoreSelectedItem(app);
}
if (resizeApp !== null) {
broadcast('startMove', {id: resizeApp.elemId, date: Date.now()});
broadcast('startResize', {id: resizeApp.elemId, date: Date.now()});
var a = {
id: app.id,
type: app.application
};
var l = {
x: parseInt(app.left, 10),
y: parseInt(app.top, 10),
width: parseInt(app.width, 10),
height: parseInt(app.height, 10)
};
addEventToUserLog(uniqueID, {type: "windowManagement",
data: {type: "move", action: "start", application: a, location: l}, time: Date.now()});
addEventToUserLog(uniqueID, {type: "windowManagement",
data: {type: "resize", action: "start", application: a, location: l}, time: Date.now()});
moveAndResizeApplicationWindow(resizeApp);
if (app.partition) {
updatePartitionInnerLayout(app.partition, true);
broadcast('partitionWindowTitleUpdate', app.partition.getTitle());
}
broadcast('finishedMove', {id: resizeApp.elemId, date: Date.now()});
broadcast('finishedResize', {id: resizeApp.elemId, date: Date.now()});
addEventToUserLog(uniqueID, {type: "windowManagement",
data: {type: "move", action: "end", application: a, location: l}, time: Date.now()});
addEventToUserLog(uniqueID, {type: "windowManagement",
data: {type: "resize", action: "end", application: a, location: l}, time: Date.now()});
}
}
function deleteApplication(appId, portalId, wsio) {
if (!SAGE2Items.applications.list.hasOwnProperty(appId)) {
return;
}
var app = SAGE2Items.applications.list[appId];
// if the app being deleted was in a partition, update partition
if (app.partition) {
let ptnId = app.partition.releaseChild(app.id)[0]; // only 1 partition effected
if (partitions.list.hasOwnProperty(ptnId)) {
// make sure this id is a partition
updatePartitionInnerLayout(partitions.list[ptnId], true);
broadcast('partitionWindowTitleUpdate', partitions.list[ptnId].getTitle());
}
}
var application = app.application;
if (application === "media_stream" || application === "media_block_stream") {
var i;
var mediaStreamData = appId.split("|");
var sender = {wsio: null, clientId: mediaStreamData[0], streamId: parseInt(mediaStreamData[1], 10)};
for (i = 0; i < clients.length; i++) {
if (clients[i].id === sender.clientId) {
sender.wsio = clients[i];
}
}
if (sender.wsio !== null) {
sender.wsio.emit('stopMediaCapture', {streamId: sender.streamId});
}
} else if (application === "JupyterLab") {
broadcast('jupyterShareTerminated', {id: appId});
}
if (app.title === "performancewidget") {
performanceManager.removeDataReceiver(appId);
}
var stickingItems = stickyAppHandler.getFirstLevelStickingItems(app);
stickyAppHandler.removeElement(app);
SAGE2Items.applications.removeItem(appId);
var im = findInteractableManager(appId);
im.removeGeometry(appId, "applications");
var widgets = SAGE2Items.widgets.list;
for (var w in widgets) {
if (widgets.hasOwnProperty(w) && widgets[w].appId === appId) {
im.removeGeometry(widgets[w].id, "widgets");
SAGE2Items.widgets.removeItem(widgets[w].id);
}
}
if (stickingItems.length > 0) {
for (var s in stickingItems) {
// When background gets deleted, sticking items stop sticking
toggleStickyPin(stickingItems[s].id);
}
} else {
// Refresh the pins on all the unpinned apps
handleStickyItem(null);
}
if (wsio) {
broadcast('userEvent', {type: 'close app', data: Object.assign(app, userlist.clients[wsio.id]), id: wsio.id});
} else {
broadcast('userEvent', {type: 'close app', data: app });
}
broadcast('deleteElement', {elemId: appId});
if (portalId !== undefined && portalId !== null) {
var ts = Date.now() + remoteSharingSessions[portalId].timeOffset;
remoteSharingSessions[portalId].wsio.emit('deleteApplication', {appId: appId, date: ts});
}
}
function pointerDraw(uniqueID, data) {
var ePos = {x: 0, y: 0};
var eUser = {id: null, label: 'drawing', color: [220, 10, 10]};
var now = Date.now();
var key;
var app;
var event;
for (key in SAGE2Items.applications.list) {
app = SAGE2Items.applications.list[key];
// Send the drawing events only to whiteboard apps
if (app.application === 'whiteboard') {
event = {id: app.id, type: "pointerDraw", position: ePos, user: eUser, data: data, date: now};
broadcast('eventInItem', event);
}
}
}
function pointerCloseGesture(uniqueID, pointerX, pointerY, time, gesture) {
if (sagePointers[uniqueID] === undefined) {
return;
}
var elem = null;
if (elem !== null) {
if (elem.closeGestureID === undefined && gesture === 0) { // gesture: 0 = down, 1 = hold/move, 2 = up
elem.closeGestureID = uniqueID;
// elem.closeGestureTime = time + closeGestureDelay; // Delay in ms
} else if (elem.closeGestureTime <= time && gesture === 1) { // Held long enough, remove
deleteApplication(elem);
} else if (gesture === 2) { // Released, reset timer
elem.closeGestureID = undefined;
}
}
}
function handleNewApplication(appInstance, videohandle) {
// Create tracking for all apps by default stacking another state load value.
// It must be done here due to how mergeObjects() works as specified in src/node-utils.js
if (appInstance.data === null || appInstance.data === undefined) {
appInstance.data = {};
}
appInstance.data.pointersOverApp = [];
broadcast('createAppWindow', appInstance);
broadcast('createAppWindowPositionSizeOnly', getAppPositionSize(appInstance));
// reserve 20 backmost layers for partitions
var zIndex = SAGE2Items.applications.numItems + SAGE2Items.portals.numItems + 20;
interactMgr.addGeometry(appInstance.id, "applications", "rectangle", {
x: appInstance.left, y: appInstance.top,
w: appInstance.width, h: appInstance.height + config.ui.titleBarHeight},
true, zIndex, appInstance);
var cornerSize = 0.2 * Math.min(appInstance.width, appInstance.height);
var oneButton = Math.round(config.ui.titleBarHeight) * (300 / 235);
var buttonsPad = 0.1 * oneButton;
var startButtons = appInstance.width - Math.round(3 * oneButton + 2 * buttonsPad);
/*
var buttonsWidth = config.ui.titleBarHeight * (324.0/111.0);
var buttonsPad = config.ui.titleBarHeight * ( 10.0/111.0);
var oneButton = buttonsWidth / 2; // two buttons
var startButtons = appInstance.width - buttonsWidth;
*/
SAGE2Items.applications.addItem(appInstance);
SAGE2Items.applications.addButtonToItem(appInstance.id, "titleBar", "rectangle",
{x: 0, y: 0, w: appInstance.width, h: config.ui.titleBarHeight}, 0);
SAGE2Items.applications.addButtonToItem(appInstance.id, "syncButton", "rectangle",
{x: startButtons, y: 0, w: oneButton, h: config.ui.titleBarHeight}, 1);
SAGE2Items.applications.addButtonToItem(appInstance.id, "fullscreenButton", "rectangle",
{x: startButtons + (1 * (buttonsPad + oneButton)), y: 0, w: oneButton, h: config.ui.titleBarHeight}, 1);
SAGE2Items.applications.addButtonToItem(appInstance.id, "closeButton", "rectangle",
{x: startButtons + (2 * (buttonsPad + oneButton)), y: 0, w: oneButton, h: config.ui.titleBarHeight}, 1);
SAGE2Items.applications.addButtonToItem(appInstance.id, "dragCorner", "rectangle", {
x: appInstance.width - cornerSize,
y: appInstance.height + config.ui.titleBarHeight - cornerSize,
w: cornerSize, h: cornerSize
}, 2);
if (appInstance.sticky === true) {
appInstance.pinned = true;
SAGE2Items.applications.addButtonToItem(appInstance.id, "pinButton", "rectangle",
{x: buttonsPad, y: 0, w: oneButton, h: config.ui.titleBarHeight}, 1);
SAGE2Items.applications.editButtonVisibilityOnItem(appInstance.id, "pinButton", false);
handleStickyItem(appInstance.id);
}
SAGE2Items.applications.editButtonVisibilityOnItem(appInstance.id, "syncButton", false);
initializeLoadedVideo(appInstance, videohandle);
if (appInstance.title === "performancewidget") {
performanceManager.addDataReceiver(appInstance.id);
}
// assign content to a partition immediately when it is created
var changedPartitions = partitions.updateOnItemRelease(appInstance);
changedPartitions.forEach((id => {
updatePartitionInnerLayout(partitions.list[id], true);
broadcast('partitionWindowTitleUpdate', partitions.list[id].getTitle());
}));
}
function handleNewApplicationInDataSharingPortal(appInstance, videohandle, portalId) {
broadcast('createAppWindowInDataSharingPortal', {portal: portalId, application: appInstance});
var zIndex = remoteSharingSessions[portalId].appCount;
var titleBarHeight = SAGE2Items.portals.list[portalId].titleBarHeight;
SAGE2Items.portals.interactMgr[portalId].addGeometry(appInstance.id, "applications", "rectangle", {
x: appInstance.left, y: appInstance.top,
w: appInstance.width, h: appInstance.height + titleBarHeight
}, true, zIndex, appInstance);
var cornerSize = 0.2 * Math.min(appInstance.width, appInstance.height);
var oneButton = Math.round(titleBarHeight) * (300 / 235);
var buttonsPad = 0.1 * oneButton;
var startButtons = appInstance.width - Math.round(3 * oneButton + 2 * buttonsPad);
/*
var buttonsWidth = titleBarHeight * (324.0/111.0);
var buttonsPad = titleBarHeight * ( 10.0/111.0);
var oneButton = buttonsWidth / 2; // two buttons
var startButtons = appInstance.width - buttonsWidth;
*/
SAGE2Items.applications.addItem(appInstance);
SAGE2Items.applications.addButtonToItem(appInstance.id, "titleBar", "rectangle",
{x: 0, y: 0, w: appInstance.width, h: titleBarHeight}, 0);
SAGE2Items.applications.addButtonToItem(appInstance.id, "syncButton", "rectangle",
{x: startButtons, y: 0, w: oneButton, h: titleBarHeight}, 1);
SAGE2Items.applications.addButtonToItem(appInstance.id, "fullscreenButton", "rectangle",
{x: startButtons + (1 * (buttonsPad + oneButton)), y: 0, w: oneButton, h: titleBarHeight}, 1);
SAGE2Items.applications.addButtonToItem(appInstance.id, "closeButton", "rectangle",
{x: startButtons + (2 * (buttonsPad + oneButton)), y: 0, w: oneButton, h: titleBarHeight}, 1);
SAGE2Items.applications.addButtonToItem(appInstance.id, "dragCorner", "rectangle", {
x: appInstance.width - cornerSize, y: appInstance.height + titleBarHeight - cornerSize,
w: cornerSize, h: cornerSize
}, 2);
if (appInstance.sticky === true) {
appInstance.pinned = true;
SAGE2Items.applications.addButtonToItem(appInstance.id, "pinButton", "rectangle",
{x: buttonsPad, y: 0, w: oneButton, h: titleBarHeight}, 1);
SAGE2Items.applications.editButtonVisibilityOnItem(appInstance.id, "pinButton", false);
handleStickyItem(appInstance.id);
}
SAGE2Items.applications.editButtonVisibilityOnItem(appInstance.id, "syncButton", false);
initializeLoadedVideo(appInstance, videohandle);
}
function handleApplicationResize(appId) {
if (SAGE2Items.applications.list[appId] === undefined) {
return;
}
var app = SAGE2Items.applications.list[appId];
var portal = findApplicationPortal(app);
var titleBarHeight = config.ui.titleBarHeight;
if (portal !== undefined && portal !== null) {
titleBarHeight = portal.data.titleBarHeight;
}
var cornerSize = 0.2 * Math.min(app.width, app.height);
var oneButton = Math.round(titleBarHeight) * (300 / 235);
var buttonsPad = 0.1 * oneButton;
var startButtons = app.width - Math.round(3 * oneButton + 2 * buttonsPad);
/*
var buttonsWidth = titleBarHeight * (324.0/111.0);
var buttonsPad = titleBarHeight * ( 10.0/111.0);
var oneButton = buttonsWidth / 2; // two buttons
var startButtons = app.width - buttonsWidth;
*/
SAGE2Items.applications.editButtonOnItem(appId, "titleBar", "rectangle",
{x: 0, y: 0, w: app.width, h: titleBarHeight});
SAGE2Items.applications.editButtonOnItem(appId, "syncButton", "rectangle",
{x: startButtons, y: 0, w: oneButton, h: titleBarHeight});
SAGE2Items.applications.editButtonOnItem(appId, "fullscreenButton", "rectangle",
{x: startButtons + (1 * (buttonsPad + oneButton)), y: 0, w: oneButton, h: titleBarHeight});
SAGE2Items.applications.editButtonOnItem(appId, "closeButton", "rectangle",
{x: startButtons + (2 * (buttonsPad + oneButton)), y: 0, w: oneButton, h: titleBarHeight});
SAGE2Items.applications.editButtonOnItem(appId, "dragCorner", "rectangle",
{x: app.width - cornerSize, y: app.height + titleBarHeight - cornerSize, w: cornerSize, h: cornerSize});
if (app.sticky === true) {
SAGE2Items.applications.editButtonOnItem(app.id, "pinButton", "rectangle",
{x: buttonsPad, y: 0, w: oneButton, h: titleBarHeight});
handleStickyItem(app.id);
}
}
function handleDataSharingPortalResize(portalId) {
if (SAGE2Items.portals.list[portalId] === undefined) {
return;
}
SAGE2Items.portals.list[portalId].scale = SAGE2Items.portals.list[portalId].width /
SAGE2Items.portals.list[portalId].natural_width;
var portalWidth = SAGE2Items.portals.list[portalId].width;
var portalHeight = SAGE2Items.portals.list[portalId].height;
var cornerSize = 0.2 * Math.min(portalWidth, portalHeight);
var oneButton = Math.round(config.ui.titleBarHeight) * (300 / 235);
var buttonsPad = 0.1 * oneButton;
var startButtons = portalWidth - Math.round(2 * oneButton + buttonsPad);
/*
var buttonsWidth = (config.ui.titleBarHeight-4) * (324.0/111.0);
var buttonsPad = (config.ui.titleBarHeight-4) * ( 10.0/111.0);
var oneButton = buttonsWidth / 2; // two buttons
var startButtons = portalWidth - buttonsWidth;
*/
SAGE2Items.portals.editButtonOnItem(portalId, "titleBar", "rectangle",
{x: 0, y: 0, w: portalWidth, h: config.ui.titleBarHeight});
SAGE2Items.portals.editButtonOnItem(portalId, "fullscreenButton", "rectangle",
{x: startButtons, y: 0, w: oneButton, h: config.ui.titleBarHeight});
SAGE2Items.portals.editButtonOnItem(portalId, "closeButton", "rectangle",
{x: startButtons + buttonsPad + oneButton, y: 0, w: oneButton, h: config.ui.titleBarHeight});
SAGE2Items.portals.editButtonOnItem(portalId, "dragCorner", "rectangle",
{x: portalWidth - cornerSize, y: portalHeight + config.ui.titleBarHeight - cornerSize, w: cornerSize, h: cornerSize});
}
function findInteractableManager(appId) {
if (interactMgr.hasObjectWithId(appId) === true) {
return interactMgr;
}
var key;
for (key in SAGE2Items.portals.interactMgr) {
if (SAGE2Items.portals.interactMgr[key].hasObjectWithId(appId) === true) {
return SAGE2Items.portals.interactMgr[key];
}
}
return null;
}
function findApplicationPortal(app) {
if (app === undefined || app === null) {
return null;
}
var portalIdx = app.id.indexOf("_portal");
if (portalIdx < 0) {
return null;
}
var portalId = app.id.substring(portalIdx + 1, app.id.length);
return interactMgr.getObject(portalId, "portals");
}
// ************** Omicron section *****************
var omicronRunning = false;
var omicronManager = new Omicron(config);
// Helper function for omicron to switch pointer mode
function omi_pointerChangeMode(uniqueID) {
remoteInteraction[uniqueID].toggleModes();
broadcast('changeSagePointerMode', {id: sagePointers[uniqueID].id, mode: remoteInteraction[uniqueID].interactionMode});
}
// Set callback functions so Omicron can generate SAGEPointer events
omicronManager.setCallbacks(
sagePointers,
createSagePointer,
showPointer,
pointerPress,
pointerMove,
pointerPosition,
hidePointer,
pointerRelease,
pointerScrollStart,
pointerScroll,
pointerScrollEnd,
pointerDblClick,
pointerCloseGesture,
keyDown,
keyUp,
keyPress,
createRadialMenu,
omi_pointerChangeMode,
undefined, // sendKinectInput
remoteInteraction);
omicronManager.linkDrawingManager(drawingManager);
/* ****** Radial Menu section ************************************************************** */
// createMediabrowser();
function createRadialMenu(uniqueID, pointerX, pointerY) {
var validLocation = true;
var newMenuPos = {x: pointerX, y: pointerY};
var existingRadialMenu = null;
// Make sure there's enough distance from other menus
for (var key in SAGE2Items.radialMenus.list) {
existingRadialMenu = SAGE2Items.radialMenus.list[key];
var prevMenuPos = {x: existingRadialMenu.left, y: existingRadialMenu.top };
var distance = Math.sqrt(Math.pow(Math.abs(newMenuPos.x - prevMenuPos.x), 2) +
Math.pow(Math.abs(newMenuPos.y - prevMenuPos.y), 2));
if (existingRadialMenu.visible && distance < existingRadialMenu.radialMenuSize.x) {
// validLocation = false;
// console.log("Menu is too close to existing menu");
}
}
if (validLocation && SAGE2Items.radialMenus.list[uniqueID + "_menu"] === undefined) {
// Create a new radial menu
var newRadialMenu = new Radialmenu(uniqueID, uniqueID, config);
newRadialMenu.generateGeometry(interactMgr, SAGE2Items.radialMenus);
newRadialMenu.setPosition(newMenuPos);
SAGE2Items.radialMenus.list[uniqueID + "_menu"] = newRadialMenu;
// Open a 'media' radial menu
broadcast('createRadialMenu', newRadialMenu.getInfo());
} else if (validLocation && SAGE2Items.radialMenus.list[uniqueID + "_menu"] !== undefined) {
// Radial menu already exists for this pointer, move to new location instead
setRadialMenuPosition(uniqueID, pointerX, pointerY);
broadcast('updateRadialMenu', existingRadialMenu.getInfo());
}
updateWallUIMediaBrowser(uniqueID);
}
/**
* Translates position of a radial menu by an offset
*
* @method moveRadialMenu
* @param uniqueID {Integer} radial menu ID
* @param pointerX {Float} offset x position
* @param pointerY {Float} offset y position
*/
function moveRadialMenu(uniqueID, pointerX, pointerY) {
var existingRadialMenu = SAGE2Items.radialMenus.list[uniqueID + "_menu"];
if (existingRadialMenu) {
existingRadialMenu.setPosition({x: existingRadialMenu.left + pointerX, y: existingRadialMenu.top + pointerY});
existingRadialMenu.visible = true;
broadcast('updateRadialMenuPosition', existingRadialMenu.getInfo());
}
}
/**
* Sets the absolute position of a radial menu
*
* @method setRadialMenuPosition
* @param uniqueID {Integer} radial menu ID
* @param pointerX {Float} x position
* @param pointerY {Float} y position
*/
function setRadialMenuPosition(uniqueID, pointerX, pointerY) {
var existingRadialMenu = SAGE2Items.radialMenus.list[uniqueID + "_menu"];
// Sets the position and visibility
existingRadialMenu.setPosition({x: pointerX, y: pointerY});
// Update the interactable geometry
interactMgr.editGeometry(uniqueID + "_menu_radial", "radialMenus", "circle",
{x: existingRadialMenu.left, y: existingRadialMenu.top, r: existingRadialMenu.radialMenuSize.y / 2});
showRadialMenu(uniqueID);
// Send the updated radial menu state to the display clients (and set menu visible)
broadcast('updateRadialMenuPosition', existingRadialMenu.getInfo());
}
/**
* Shows radial menu and enables interactivity
*
* @method showRadialMenu
* @param uniqueID {Integer} radial menu ID
*/
function showRadialMenu(uniqueID) {
var radialMenu = SAGE2Items.radialMenus.list[uniqueID + "_menu"];
if (radialMenu !== undefined) {
radialMenu.visible = true;
interactMgr.editVisibility(uniqueID + "_menu_radial", "radialMenus", true);
interactMgr.editVisibility(uniqueID + "_menu_thumbnail", "radialMenus", radialMenu.isThumbnailWindowOpen());
}
}
/**
* Hides radial menu and enables interactivity
*
* @method hideRadialMenu
* @param uniqueID {Integer} radial menu ID
*/
function hideRadialMenu(uniqueID) {
var radialMenu = SAGE2Items.radialMenus.list[uniqueID + "_menu"];
if (radialMenu !== undefined) {
radialMenu.hide();
}
broadcast('updateRadialMenu', radialMenu.getInfo());
}
function updateWallUIMediaBrowser(uniqueID) {
var list = getSavedFilesList();
broadcast('updateRadialMenuDocs', {id: uniqueID, fileList: list});
}
// Sends button state update messages to display
function radialMenuEvent(data) {
if (data.type === "stateChange") {
broadcast('radialMenuEvent', data);
if (data.menuState.action !== undefined && data.menuState.action.type === "saveSession") {
var ad = new Date();
var sname = sprint("session_%4d_%02d_%02d_%02d_%02d_%02s",
ad.getFullYear(), ad.getMonth() + 1, ad.getDate(),
ad.getHours(), ad.getMinutes(), ad.getSeconds());
saveSession(sname);
} else if (data.menuState.action !== undefined && data.menuState.action.type === "tileContent") {
tileApplications();
} else if (data.menuState.action !== undefined && data.menuState.action.type === "clearAllContent") {
clearDisplay();
}
} else {
broadcast('radialMenuEvent', data);
}
}
// Check for pointer move events that are dragging a radial menu (but outside the menu)
function updateRadialMenuPointerPosition(uniqueID, pointerX, pointerY) {
for (var key in SAGE2Items.radialMenus.list) {
var radialMenu = SAGE2Items.radialMenus.list[key];
// console.log(data.id+"_menu: " + radialMenu);
if (radialMenu !== undefined && radialMenu.dragState === true) {
var offset = radialMenu.getDragOffset(uniqueID, {x: pointerX, y: pointerY});
moveRadialMenu(radialMenu.id, offset.x, offset.y);
}
}
}
function wsRemoveRadialMenu(wsio, data) {
hideRadialMenu(data.id);
}
function wsRadialMenuThumbnailWindow(wsio, data) {
var radialMenu = SAGE2Items.radialMenus.list[data.id + "_menu"];
if (radialMenu !== undefined) {
radialMenu.openThumbnailWindow(data);
var thumbnailWindowPos = radialMenu.getThumbnailWindowPosition();
interactMgr.editGeometry(data.id + "_menu_thumbnail", "radialMenus", "rectangle", {
x: thumbnailWindowPos.x,
y: thumbnailWindowPos.y,
w: radialMenu.thumbnailWindowSize.x,
h: radialMenu.thumbnailWindowSize.y
});
interactMgr.editVisibility(data.id + "_menu_thumbnail", "radialMenus", data.thumbnailWindowOpen);
}
}
function wsRadialMenuMoved(wsio, data) {
var radialMenu = SAGE2Items.radialMenus.list[data.uniqueID + "_menu"];
if (radialMenu !== undefined) {
radialMenu.setPosition(data);
}
}
/**
* Called when an item is dropped after a move, and when a sticky item pin is toggled. This method
* checks attaching of sticky items to background items and detaching previously attached
* sticky items from background items (when the are moved away). It also handles hiding of pins of
* items not pinned when their background is removed from underneath them
*/
function handleStickyItem(elemId) {
var app = SAGE2Items.applications.list[elemId];
var im;
if (elemId !== null && app !== null && app !== undefined && app.sticky === true) {
stickyAppHandler.detachStickyItem(app);
im = findInteractableManager(elemId);
var backgroundObj = im.getBackgroundObj(app, null);
if (backgroundObj === null) {
hideStickyPin(app);
} else if (SAGE2Items.applications.list.hasOwnProperty(backgroundObj.data.id)) {
var backgroundApp = SAGE2Items.applications.list[backgroundObj.data.id];
if (app.pinned === true) {
stickyAppHandler.attachStickyItem(backgroundApp, app);
} else {
stickyAppHandler.registerNotPinnedApp(app);
}
showStickyPin(app);
}
}
var appsNotPinned = stickyAppHandler.getNotPinnedAppList();
var appsNotPinnedWithBackground = [];
for (var i in appsNotPinned) {
var tmpAppVariable = SAGE2Items.applications.list[appsNotPinned[i].id];
if (tmpAppVariable === null || tmpAppVariable === undefined) {
//Apps on this list might have been deleted
continue;
}
im = findInteractableManager(tmpAppVariable.id);
if (im.getBackgroundObj(tmpAppVariable, null) === null) {
//If there is no background hide the pin
hideStickyPin(tmpAppVariable);
} else {
//If there is a background, continue to maintain the app on the not pinned list
appsNotPinnedWithBackground.push(tmpAppVariable);
}
}
stickyAppHandler.refreshNotPinnedAppList(appsNotPinnedWithBackground);
}
/**
* Called when user clicks on a sticky item pin. This method toggles the status of the pin.
*/
function toggleStickyPin(appId) {
var app = SAGE2Items.applications.list[appId];
if (app === null || app === undefined || app.sticky !== true) {
return;
}
if (app.hasOwnProperty("pinned") === false || app.pinned !== true) {
app.pinned = true;
} else {
app.pinned = false;
stickyAppHandler.registerNotPinnedApp(app);
}
handleStickyItem(app.id);
}
function showStickyPin(app) {
SAGE2Items.applications.editButtonVisibilityOnItem(app.id, "pinButton", true);
// only send required fields (sending full app can throw error from circular JSON
// if it is in a Partition -- I assume it could happen in other cases as well)
broadcast('showStickyPin', {
id: app.id,
sticky: app.sticky,
pinned: app.pinned
});
}
function hideStickyPin(app) {
SAGE2Items.applications.editButtonVisibilityOnItem(app.id, "pinButton", false);
// only send required fields (sending full app can throw error from circular JSON
// if it is in a Partition -- I assume it could happen in other cases as well)
broadcast('hideStickyPin', {
id: app.id,
sticky: app.sticky
});
}
function showOrHideWidgetLinks(data) {
var obj = data.item;
var appId = obj.id;
if (obj.data !== undefined && obj.data !== null && obj.data.appId !== undefined) {
appId = obj.data.appId;
}
var app = SAGE2Items.applications.list[appId];
if (app !== null && app !== undefined) {
app = getAppPositionSize(app);
app.user_id = data.uniqueID;
if (data.show === true) {
app.user_color = data.user_color;
if (app.user_color !== null) {
appUserColors[appId] = app.user_color;
}
broadcast('showWidgetToAppConnector', app);
} else {
broadcast('hideWidgetToAppConnector', app);
}
}
}
/**
* Asks server for the context menu of an app. Server will send the current known menu.
* Menus must be sumitted by app, or the default "Not yet loaded" will be displayed.
* To use context menu an app MUST have been loaded on a master display.
*
* @method wsRequestAppContextMenu
* @param {Object} wsio - The websocket of sender.
* @param {Object} data - The object needed to get menu, properties described below.
* @param {Integer} data.x - Pointer x, corresponds to on entire wall.
* @param {Integer} data.y - Pointer y, corresponds to on entire wall.
* @param {Integer} data.xClick - Where client clicked on their screen, because this is async.
* @param {Integer} data.yClick - Where client clicked on their screen, because this is async.
*/
function wsRequestAppContextMenu(wsio, data) {
// First find if there is an app at location, top most element.
var obj = interactMgr.searchGeometry({x: data.x, y: data.y});
if (obj !== null) {
// Check if it was an application
if (SAGE2Items.applications.list.hasOwnProperty(obj.data.id)) {
// If an app was under the right-click
if (SAGE2Items.applications.list[obj.data.id].contextMenu) {
// Before passing back the menu, fill in the share options.
let contextMenu = SAGE2Items.applications.list[obj.data.id].contextMenu;
fillContextMenuWithShareSites(contextMenu, obj.data.id);
// If we already have the menu info, send it
wsio.emit('appContextMenuContents', {
x: data.xClick,
y: data.yClick,
app: obj.data.id,
entries: contextMenu
});
} else { // Else, app did not submit menu, give default (not loaded).
wsio.emit('appContextMenuContents', {
x: data.xClick,
y: data.yClick,
app: obj.data.id,
entries: [{
description: "App not yet loaded on display client yet."
}]
});
} // otherwise if it was a partition
} else if (partitions.list.hasOwnProperty(obj.data.id)) {
// if a partition was under the right-click
wsio.emit('appContextMenuContents', {
x: data.xClick,
y: data.yClick,
app: obj.data.id,
entries: partitions.list[obj.data.id].getContextMenu()
});
}
}
}
/**
* Given a context menu, will fill with appropriate share sites.
*
* @method fillContextMenuWithShareSites
* @param {Object} contextMenu - The context menu of the application.
* @param {String} appId - Unique string to get app, SAGE2Items.applications.list[appId].
*/
function fillContextMenuWithShareSites(contextMenu, appId) {
let shareIndex = -1;
let shareDescription = "Share With:"; // match for this description
let entry;
// first search for the share entry
for (let i = 0; i < contextMenu.length; i++) {
if (contextMenu[i].description === shareDescription) {
shareIndex = i;
}
}
// if there was no share entry, they need to add it
if (shareIndex === -1) {
entry = { description: "separator" };
contextMenu.splice(contextMenu.length - 3, 0, entry); // add separator after the maximize entry
entry = { description: shareDescription };
contextMenu.splice(contextMenu.length - 3, 0, entry); // add share option after that
} else { // otherwise get the reference
entry = contextMenu[shareIndex];
}
entry.children = []; // clear out the sites, there may have been a status change.
// for each remote site, check if connected, if so add a share option
for (let i = 0; i < remoteSites.length; i++) {
if (remoteSites[i].connected === "on") {
entry.children.push({
description: remoteSites[i].name,
callback: "SAGE2_shareWithSite",
parameters: { app: appId, siteName: remoteSites[i].name, remoteSiteIndex: i }
});
}
}
}
/**
* Received from a display client, apps will send their context menu after completing their initialization.
*
* @method wsAppContextMenuContents
* @param {Object} wsio - The websocket of sender.
* @param {Object} data - The object properties described below.
* @param {String} data.app - App id that this menu is for.
* @param {Array} data.entries - Array of objects describing the entries.
*/
function wsAppContextMenuContents(wsio, data) {
SAGE2Items.applications.list[data.app].contextMenu = data.entries;
}
/**
* Will call a function on each display client's app that matches id. Expected usage with context menu.
* But can be used in other cases.
* There are some special functionality cases like:
* SAGE2DeleteElement, SAGE2SendToBack, SAGE2Maximize
* These do not send message to app.
*
* @method wsCallFunctionOnApp
* @param {Object} wsio - The websocket of sender.
* @param {Object} data - The object properties described below.
* @param {Integer} data.x - Pointer x, corresponds to on entire wall.
* @param {Integer} data.y - Pointer y, corresponds to on entire wall.
* @param {String} data.app - App id, which function should be activated.
* @param {String} data.func - Name of function to activate
* @param {Object} data.parameters - Object to send to the app as parameter.
*/
function wsCallFunctionOnApp(wsio, data) {
// check if the id is an applications or partition
if (SAGE2Items.applications.list.hasOwnProperty(data.app)) {
// check for special cases, no message sent to app.
if (data.func === "SAGE2DeleteElement") {
deleteApplication(data.app, null, wsio);
return;
} else if (data.func === "SAGE2SendToBack") {
// data.app should contain the id.
var im = findInteractableManager(data.app);
im.moveObjectToBack(data.app, "applications");
var newOrder = im.getObjectZIndexList("applications");
broadcast('updateItemOrder', newOrder);
return;
} else if (data.func === "SAGE2Maximize") {
if (data.parameters.clientId && SAGE2Items.applications.list[data.app]) {
toggleApplicationFullscreen(data.parameters.clientId,
SAGE2Items.applications.list[data.app],
true);
}
return;
} else if (data.func === "SAGE2_shareWithSite"
&& (remoteSites[data.parameters.remoteSiteIndex].connected === "on")) {
// share this application with a site.
let uniqueID = wsio.id;
// the release
let app = {application: SAGE2Items.applications.list[data.app]};
let remote = remoteSites[data.parameters.remoteSiteIndex];
shareApplicationWithRemoteSite(uniqueID, app, remote);
return;
}
// Using broadcast means the parameter must be in data.data
data.data = data.parameters;
// add the serverDate property
data.data.serverDate = Date.now();
// add the clientId property
data.data.clientId = wsio.id;
// send to all display clients(since they all need to update)
for (var i = 0; i < clients.length; i++) {
if (clients[i].clientType === "display") {
clients[i].emit('broadcast', data);
}
}
} else if (partitions.list.hasOwnProperty(data.app)) {
// the context menu is on a partition
let ptn = partitions.list[data.app];
if (data.func === "SAGE2DeleteElement") {
deletePartition(data.app);
// closing of applications are handled by the called function.
return;
} else if (data.func === "SAGE2Maximize") {
if (data.parameters.clientId) {
if (!ptn.maximized) {
remoteInteraction[data.parameters.clientId].maximizeSelectedItem(ptn);
} else {
remoteInteraction[data.parameters.clientId].restoreSelectedItem(ptn);
}
partitions.updatePartitionGeometries(data.app, interactMgr);
broadcast('partitionMoveAndResizeFinished', ptn.getDisplayInfo());
// update neighbors if it is snapped
if (ptn.isSnapping) {
let updatedNeighbors = ptn.updateNeighborPtnPositions();
// update geometries/display/layout of any updated neighbors
for (var neigh of updatedNeighbors) {
partitions.updatePartitionGeometries(neigh, interactMgr);
broadcast('partitionMoveAndResizeFinished', partitions.list[neigh].getDisplayInfo());
updatePartitionInnerLayout(partitions.list[neigh], true);
}
}
// update child positions within partiton
updatePartitionInnerLayout(ptn, false);
}
} else if (data.func === "clearPartition") {
// invoke clear with delete application method -- messy, should refactor
partitions.list[data.app][data.func](deleteApplication);
} else if (data.func === "toggleSnapping" || data.func === "updateNeighborPartitionList") {
let updatedNeighbors = partitions.list[data.app][data.func]();
broadcast('updatePartitionSnapping', partitions.list[data.app].getDisplayInfo());
for (let p of updatedNeighbors) {
if (partitions.list[p]) {
broadcast('updatePartitionSnapping', partitions.list[p].getDisplayInfo());
}
}
} else if (data.func === "setColor") {
partitions.list[data.app][data.func](data.parameters.clientInput);
broadcast('updatePartitionColor', partitions.list[data.app].getDisplayInfo());
} else {
// invoke the other callback
partitions.list[data.app][data.func](data.parameters);
}
updatePartitionInnerLayout(partitions.list[data.app], true);
broadcast('partitionWindowTitleUpdate', partitions.list[data.app].getTitle());
}
}
/**
* Will launch app with specified name and call the given function after.
* The function doesn't need to be called to give the parameters.
*
* @method wsLaunchAppWithValues
* @param {Object} wsio - The websocket of sender.
* @param {Object} data - The object properties described below.
* @param {String} data.appName - Folder name to check for the app.
* @param {Object} data.params - Will be passed to the app. Function too, it is specified.
* @param {String} data.func - Optional, if specified, will also call this funciton and pass parameters.
*/
function wsLaunchAppWithValues(wsio, data) {
// first try see if the app is registered with apps exif.
var fullpath = appLaunchHelperGetPathOfApp(data.appName);
// null means not found, try check if folder path exists.
if (fullpath === null) {
fullpath = path.join(mediaFolders.system.path, "apps", data.appName);
try {
fs.accessSync(fullpath);
} catch (err) {
sageutils.log("wsLaunchAppWithValues", "Cannot launch", data.appName, ", doesn't exist.");
return;
}
}
// Prep the data needed to launch an application.
var appLoadData = { };
appLoadData.application = fullpath;
appLoadData.user = wsio.id; // needed for the wsLoadApplication function
appLoadData.wasPositionGivenInMessage = false;
appLoadData.wasLaunchedThroughMessage = true;
if (data.customLaunchParams) {
appLoadData.customLaunchParams = data.customLaunchParams;
appLoadData.customLaunchParams.serverDate = Date.now();
appLoadData.customLaunchParams.clientId = wsio.id;
appLoadData.customLaunchParams.parent = data.app;
appLoadData.customLaunchParams.functionToCallAfterInit = data.func;
} else {
appLoadData.customLaunchParams = {parent: data.app};
}
// If the launch location is defined, use it, otherwise use the stagger position.
if (data.xLaunch !== null && data.xLaunch !== undefined) {
appLoadData.position = [data.xLaunch, data.yLaunch];
appLoadData.wasPositionGivenInMessage = true;
}
// get this before the app is created. id start from 0. count is the next one
var whatTheNewAppIdShouldBe = "app_" + getUniqueAppId.count;
// call the previously made wsLoadApplication funciton and give it the required data.
wsLoadApplication(wsio, appLoadData);
// notify the creating app(if any) child's id, if undefined, then the display doesn't do anything with it
broadcast('broadcast', {app: data.app, func: "addToAppsLaunchedList", data: whatTheNewAppIdShouldBe});
} // end wsLaunchAppWithValues
/**
* Used to get the full path of an app starting with appName in the FileName.
*
* @method appLaunchHelperGetPathOfApp
* @param {Object} appName - Folder name to check for the app.
* @return {String|null} Either it gets the full path or null to indicate not available.
*/
function appLaunchHelperGetPathOfApp(appName) {
var apps = assets.listApps();
// for each of the apps known to SAGE2, usually everything in public/uploads/apps
for (var i = 0; i < apps.length; i++) {
if (// if the name contains appName
apps[i].exif.FileName.indexOf(appName) === 0
|| apps[i].id.indexOf(appName) !== -1
) {
return apps[i].id; // this is the path.
} // end if this app contains the specified name
} // end for each application available.
return null;
}
/**
* Sends data to a specific client or set.
*
* @method wsSendDataToClient
* @param {Object} wsio - The websocket of sender.
* @param {Object} data - The object properties described below.
* @param {String} data.clientDest - Unique identifier of client
*/
function wsSendDataToClient(wsio, data) {
var i;
if (data.clientDest === "allDisplays") {
for (i = 0; i < clients.length; i++) {
if (clients[i].clientType === "display") {
clients[i].emit('broadcast', data);
}
}
} else if (data.clientDest === "masterDisplay") {
// only send if a master display is connected
if (masterDisplay) {
masterDisplay.emit('broadcast', data); // only send to one display to prevent multiple responses.
}
} else {
for (i = 0; i < clients.length; i++) {
// !!!! the clients[i].id and clientDest need auto convert to evaluate as equivalent.
// update: condition is because console.log auto converts in a specific way
if (clients[i].id == data.clientDest) {
clients[i].emit('sendDataToClient', data);
}
}
}
}
/**
* Used to write files into the media folders.
* Writes to a joined location of mainFolder.path(~/Documents/SAGE2_media)
*
* @method wsSaveDataOnServer
* @param {Object} wsio - The websocket of sender.
* @param {Object} data - The object properties described below.
* @param {String} data.fileName - Name of the file.
* @param {String} data.fileType - Extension of the file.
* @param {String} data.fileContent - To be written in the file.
*/
function wsSaveDataOnServer(wsio, data) {
// First check if all necessary fields have been provided.
if (data.fileType == null || data.fileType == undefined
|| data.fileName == null || data.fileName == undefined
|| data.fileContent == null || data.fileContent == undefined
) {
sageutils.log("wsSaveDataOnServer", "ERROR> not saving data, a required field is null or undefined");
}
// Remove any path changing by chopping off the / andor \ in the filename.
while (data.fileName.indexOf("/") >= 0) {
data.fileName = data.fileName.substring(data.fileName.indexOf("/") + 1);
}
while (data.fileName.indexOf("\\") >= 0) {
data.fileName = data.fileName.substring(data.fileName.indexOf("\\") + 1);
}
// Create the notes folder as needed
var notesFolder = path.join(mainFolder.path, "notes");
if (!sageutils.folderExists(notesFolder)) {
sageutils.mkdirParent(notesFolder);
}
var fullpath;
// Specific checks for each file type (extension)
if (data.fileType === "note") {
// Just in case, save
fullpath = path.join(notesFolder, "lastNote.note");
fs.writeFileSync(fullpath, data.fileContent);
fullpath = path.join(notesFolder, data.fileName);
fs.writeFileSync(fullpath, data.fileContent);
} else if (data.fileType === "doodle") {
// Just in case, save
fullpath = path.join(notesFolder, "lastDoodle.doodle");
// Remove the header but keep uri
var regex = /^data:.+\/(.+);base64,(.*)$/;
var matches = data.fileContent.match(regex);
// Convert to base64 encoding
var buffer = new Buffer(matches[2], 'base64');
fs.writeFileSync(fullpath, buffer);
fullpath = path.join(notesFolder, data.fileName);
fs.writeFileSync(fullpath, buffer);
} else if (data.fileType === "png") {
fullpath = path.join(mainFolder.path, "images", data.fileName);
var pngBuffer = new Buffer(data.fileContent, "base64");
fs.writeFileSync(fullpath, pngBuffer);
} else if (data.fileType === "jpg") {
fullpath = path.join(mainFolder.path, "images", data.fileName);
var jpgBuffer = new Buffer(data.fileContent);
fs.writeFileSync(fullpath, jpgBuffer);
} else if (data.fileType === "tmp") {
fullpath = path.join(mainFolder.path, "tmp", data.fileName);
var aBuffer = new Buffer(data.fileContent);
fs.writeFileSync(fullpath, aBuffer);
} else {
sageutils.log("wsSaveDataOnServer", "ERROR> unable to save fileType", data.fileType);
}
}
/**
* Sets the value of specified server data. If it doesn't exist, will create it.
*
* @method wsServerDataSetValue
* @param {Object} wsio - The websocket of sender.
* @param {Object} data - The object properties described below.
*/
function wsServerDataSetValue(wsio, data) {
sharedServerData.setValue(wsio, data);
}
/**
* Checks if there is a value, and if so will send the value.
* If the value doesn't exist, it will not return anything.
*
* @method wsServerDataGetValue
* @param {Object} wsio - The websocket of sender.
* @param {Object} data - The object properties described below.
*/
function wsServerDataGetValue(wsio, data) {
sharedServerData.getValue(wsio, data);
}
/**
* Removes variable from server. Expected usage is this is called when an app closes.
* Made for the sake of cleanup as apps open and close.
*
* @method wsServerDataRemoveValue
* @param {Object} wsio - The websocket of sender.
* @param {Object} data - The object properties described below.
*/
function wsServerDataRemoveValue(wsio, data) {
sharedServerData.removeValue(wsio, data);
}
/**
* Add the app to the named values a subscriber.
* If the value doesn't exist, it will create a "blank" value and subscribe to it.
*
* @method wsServerDataSubscribeToValue
* @param {Object} wsio - The websocket of sender.
* @param {Object} data - The object properties described below.
* @param {String} data.nameOfValue - Name of value to subscribe to.
* @param {String} data.app - App that requested.
* @param {String} data.func - Name of the function on the app to give value to.
*/
function wsServerDataSubscribeToValue(wsio, data) {
sharedServerData.subscribeToValue(wsio, data);
}
/**
* Will respond back once to the app giving the func an array of tracked values.
* They will be in an array of objects with properties nameOfValue and value.
* NOTE: the values in the array could be huge.
*
* @method wsServerDataGetAllTrackedValues
* @param {Object} wsio - The websocket of sender.
* @param {Object} data - The object properties described below.
* @param {String} data.app - App that requested.
* @param {String} data.func - Name of the function on the app to give value to.
*/
function wsServerDataGetAllTrackedValues(wsio, data) {
sharedServerData.getAllTrackedValues(wsio, data);
}
/**
* Will respond back once to the app giving the func an array of tracked descriptions.
* They will be in an array of objects with properties nameOfValue and description.
*
* @method wsServerDataGetAllTrackedDescriptions
* @param {Object} wsio - The websocket of sender.
* @param {Object} data - The object properties described below.
* @param {String} data.app - App that requested.
* @param {String} data.func - Name of the function on the app to give value to.
*/
function wsServerDataGetAllTrackedDescriptions(wsio, data) {
sharedServerData.getAllTrackedDescriptions(wsio, data);
}
/**
* Will add the websocket to subscriber list of new value notifications.
* The subscriber will get an object with nameOfValue and description.
*
* @method wsServerDataSubscribeToNewValueNotification
* @param {Object} wsio - The websocket of sender.
* @param {Object} data - The object properties described below.
* @param {String} data.app - App that requested.
* @param {String} data.func - Name of the function on the app to give value to.
*/
function wsServerDataSubscribeToNewValueNotification(wsio, data) {
sharedServerData.subscribeToNewValueNotification(wsio, data);
}
/**
* Calculate if we have enough screenshot-capable display clients
* and send message to UI clients to enable the screenshot menu
*
* @method ReportIfCanWallScreenshot
*/
function reportIfCanWallScreenshot() {
var numOfDisplayClients = config.displays.length;
var canWallScreenshot = 0;
// check if all display clients can take a screenshot
for (let i = 0; i < clients.length; i++) {
if (clients[i].clientType === "display" && clients[i].capableOfScreenshot === true) {
canWallScreenshot++;
}
}
// Send the news to the UI clients
broadcast("reportIfCanWallScreenshot", {
capableOfScreenshot: (canWallScreenshot === numOfDisplayClients)
});
}
/**
* Sent from UI, server gets it and tells displays to send back screenshots.
* Only happens if a screenshot is not already in progress to prevent spam.
* The masterDisplay check in array is reset (discarded) and rebuilt.
*
* @method wsStartWallScreenshot
* @param {Object} wsio The websocket
* @param {Object} data The data
*/
function wsStartWallScreenshot(wsio, data) {
// If not already taking a screen shot, then can emit, to prevent spamming
if (masterDisplay.startedScreenshot === undefined || masterDisplay.startedScreenshot === false) {
// Need and additional tracking variable to prevent multiple users
// from spamming the screenshot command
masterDisplay.startedScreenshot = true;
// [x][y] the previous array is discarded
masterDisplay.displayCheckIn = [];
for (var x = 0; x < config.layout.columns; x++) {
for (var y = 0; y < config.layout.rows; y++) {
var idx = y * config.layout.columns + x;
// Set to false
masterDisplay.displayCheckIn[idx] = false;
}
}
// then send the messages
for (var i = 0; i < clients.length; i++) {
if (clients[i].clientType === "display") {
// Their submission status is reset
clients[i].submittedScreenshot = false;
// Necessary to ignore other displays
if (clients[i].capableOfScreenshot === undefined) {
// Capabilities are set on response
clients[i].capableOfScreenshot = true;
}
clients[i].emit("sendServerWallScreenshot");
}
}
}
}
/**
* Called when displays are sending screenshots.
* Displays that are not capable of screenshots will report back saying so.
* Performs the following:
* if not display, stop
* if display is not capable, mark status, stop
* get all displays in array
* save the current display's screenshot
* mark this display in the correct check in position
* if display has width and height, mark those locations too
* if all display tiles screenshots have been submitted
* OR all displays have submitted a screenshot or are incapable
* then make a screenshot
* done with mosaic and offset tiles based on config information
* stitching is done in tmp folder to avoid problems caused by the folder monitor
* finally reset variable to allow another screenshot
*
*
* @method wsWallScreenshotFromDisplay
* @param {Object} wsio The websocket
* @param {Object} data The data
*/
function wsWallScreenshotFromDisplay(wsio, data) {
if (wsio.clientType != "display") {
// Something incorrect happened for a non-display to submit a screenshot
return;
}
// Check if the responded display was capable in the first place
if (!data.capable) {
wsio.capableOfScreenshot = false;
// Can't do anything if the display isn't capable
return;
}
// Declaring reused variables here
var xDisplay, yDisplay;
var i, x, y, idx, id;
// First get all connected display clients
var allDisplaysFromClients = [];
for (i = 0; i < clients.length; i++) {
if (clients[i].clientType === "display") {
allDisplaysFromClients.push(clients[i]);
}
}
// First create information necessary to save the file
var fileSaveObject = {};
// client ID in this case refers to the display clientID url param. 0 by default
// TODO: mirror overwrite is possible, is bad?
fileSaveObject.fileName = "wallScreenshot_" + wsio.clientID + ".jpg";
fileSaveObject.fileType = "tmp";
fileSaveObject.fileContent = data.imageData;
// Create the current tile piece and tile pieces individually saved
wsSaveDataOnServer(wsio, fileSaveObject);
// Set the tracking variables for the tile piece
// Mark itself as having submitted a screenshot
wsio.submittedScreenshot = true;
//
// This whole next section is about proper placement into the displayCheckIn[x][y]
// Necessary because of systems that define a single display as having width and height
//
if (masterDisplay.displayCheckIn[wsio.clientID] != undefined) {
idx = config.displays[wsio.clientID].row * config.layout.columns + config.displays[wsio.clientID].column;
masterDisplay.displayCheckIn[idx] = wsio;
// set this wsio for each of the spaces (client covering several tiles)
for (x = 0; x < config.displays[wsio.clientID].width; x++) {
for (y = 0; y < config.displays[wsio.clientID].height; y++) {
xDisplay = config.displays[wsio.clientID].column + x;
yDisplay = config.displays[wsio.clientID].row + y;
idx = yDisplay * config.layout.columns + xDisplay;
masterDisplay.displayCheckIn[idx] = wsio;
}
}
} else {
sageutils.log('Screenshot', "Unknown display", wsio.clientID, "checked in for screenshot");
}
//
// Now check if everyone submitted.
// NOTE: very possible to have timing issues.
// Counting on the fact that screenshot takes more time the non-capable response
//
// Display check in necessary for weird pieces
//
var allDisplaysSubmittedScreenshots = true;
// First check if each of the tiles in the wall have been filled
for (i = 0; i < masterDisplay.displayCheckIn.length; i++) {
if (masterDisplay.displayCheckIn[i] === false) {
allDisplaysSubmittedScreenshots = false;
break;
}
}
// If there is a missing piece from the tiles, possible that there is no active display for it.
if (!allDisplaysSubmittedScreenshots) {
// Reset to true, it will be false if there is a missing piece
allDisplaysSubmittedScreenshots = true;
for (i = 0; i < allDisplaysFromClients.length; i++) {
// Check if the display is capable
if (allDisplaysFromClients[i].capableOfScreenshot) {
// Check it hasn't submitted a screenshot, don't have all tiles
if (!allDisplaysFromClients[i].submittedScreenshot) {
allDisplaysSubmittedScreenshots = false;
break;
}
}
}
}
// Stop if not all displays submitted.
// Return here to prevent too many nested blocks
if (!allDisplaysSubmittedScreenshots) {
return;
}
// At this point ready to make a screen shot
// First need the date to use as a unique name modifier
var dateSuffix = formatDateToYYYYMMDD_HHMMSS(new Date());
// More than 1 tile means that stitching needs to be applied
if (allDisplaysFromClients.length > 1) {
// Stitching needs to be done by rows
// Tile pieces are still saved in images
var basePath = path.join(mainFolder.path, "tmp");
var currentPath;
var xMosaicPosition = 0;
var yMosaicPosition = 0;
var mosaicImage = imageMagick().in("-background", "black");
// var tilesUsed = [];
// var needToSkip;
// For each element in the display checkin
// if it is false, then the display isn't connected
// but check if the wsio was already used
// because if it was used, that display has width / height greater than 1 tile
// so it needs to be skipped
// tiles that dont need to be skipped will have their temp file referenced with offet of tile position * resolution
for (i = 0; i < masterDisplay.displayCheckIn.length; i++) {
// Calculate the coordinates
id = masterDisplay.displayCheckIn[i].clientID;
x = config.displays[id].column;
y = config.displays[id].row;
idx = y * config.layout.columns + x;
xMosaicPosition = x * config.resolution.width;
yMosaicPosition = y * config.resolution.height;
currentPath = path.join(basePath, "wallScreenshot_" + id + ".jpg");
mosaicImage = mosaicImage.in("-page", "+" + xMosaicPosition + "+" + yMosaicPosition);
mosaicImage = mosaicImage.in(currentPath);
}
// Setting the output into the tmp folder
var fname = "screenshot-" + dateSuffix + ".jpg";
currentPath = path.join(mainFolder.path, "tmp", fname);
// Ready for mosaic and write
mosaicImage.mosaic().quality(90).write(currentPath, function(error) {
if (error) {
sageutils.log('Screenshot', error);
} else {
// Add the image into the asset management and open with a width 1/4 of the wall
manageUploadedFiles([{
// output folder
path: currentPath,
// filename
name: fname}],
// position and size
[0, 0, config.totalWidth / 4],
// username and color
"screenshot", "#B4B4B4",
// to be opened afterward
true);
// Delete the temporary files
sageutils.deleteFiles(path.join(mainFolder.path, "tmp", "wallScreenshot_*"));
}
});
} else {
// Just change the name
fileSaveObject.fileName = "screenshot-" + dateSuffix + ".jpg";
wsSaveDataOnServer(wsio, fileSaveObject);
// Add the image into the asset management and open with a width 1/4 of the wall
manageUploadedFiles([{
// output folder
path: path.join(mainFolder.path, "tmp", fileSaveObject.fileName),
// file name
name: fileSaveObject.fileName}],
// position and size
[0, 0, config.totalWidth / 4],
// username and color
"screenshot", "#B4B4B4",
// to be opened afterward
true);
// Delete the temporary files
sageutils.deleteFiles(path.join(mainFolder.path, "tmp", "wallScreenshot_*"));
}
// Reset variable to allow another capture
masterDisplay.startedScreenshot = false;
}
/**
* Receive data from Electron display client about their hardware
*
* @method wsDisplayHardware
* @param {<type>} wsio The wsio
* @param {<type>} data The data
*/
function wsDisplayHardware(wsio, data) {
// store the hardware data for a given client
performanceManager.addDisplayClient(wsio.id, wsio.clientID, data);
}
/**
* Receive data from Electron display client about their hardware
*
* @method wsPerformanceData
* @param {<type>} wsio The wsio
* @param {<type>} data The data
*/
function wsPerformanceData(wsio, data) {
// Pass the performance data from Electron display client
// on the performance manager
performanceManager.saveDisplayPerformanceData(wsio.id, wsio.clientID, data);
}
/**
* Start a jupyter connection
*
* @method wsStartJupyterSharing
* @param {Object} wsio The websocket
* @param {Object} data The data
*/
function wsStartJupyterSharing(wsio, data) {
sageutils.log('Jupyter', "received new stream:", data.id);
/*var i;
SAGE2Items.renderSync[data.id] = {clients: {}, chunks: []};
for (i = 0; i < clients.length; i++) {
if (clients[i].clientType === "display") {
SAGE2Items.renderSync[data.id].clients[clients[i].id] = {wsio: clients[i], readyForNextFrame: false, blocklist: []};
}
}
*/
console.log(data.width, data.height);
// forcing 'int' type for width and height
data.width = parseInt(data.width, 10);
data.height = parseInt(data.height, 10);
// appLoader.createJupyterApp(data.src, data.type, data.encoding, data.title, data.color, 800, 1200,
// function(appInstance) {
// appInstance.id = data.id;
// handleNewApplication(appInstance, null);
// }
// );
console.log(data.id);
console.log("createJupyterApp: ", data.src, data.type, data.encoding, data.title, data.color, data.width, data.height);
appLoader.createJupyterApp(data.src, data.type, data.encoding, data.title, data.color, data.width, data.height,
function (appInstance) {
appInstance.id = data.id;
handleNewApplication(appInstance, null);
console.log(appInstance.id);
}
);
}
function wsUpdateJupyterSharing(wsio, data) {
sageutils.log('Jupyter', "received update from:", data.id);
// pass data into app by ID
sendApplicationDataUpdate(data);
}
function sendApplicationDataUpdate(data) {
var eUser = { id: 1, label: "Touch", color: "none" };
var event = {
id: data.id,
type: "dataUpdate",
position: 0,
user: eUser,
data: data,
date: Date.now()
};
broadcast('eventInItem', event);
}
/*
function wsUpdateJupyterSharing(wsio, data) {
sageutils.log('Jupyter', "received update from:", data.id);
sendJupyterUpdates(data);
}
function sendJupyterUpdates(data) {
// var ePosition = {x: 0, y: 0};
var eUser = {id: 1, label: "Touch", color: "none"};
var event = {
id: data.id,
type: "imageUpload",
position: 0,
user: eUser,
data: data,
date: Date.now()
};
broadcast('eventInItem', event);
}
*/
/**
* Method handling a file save request from a SAGE2_App
*
* @method appFileSaveRequest
* @param {Object} wsio The websocket
* @param {Object} data The data
*/
function appFileSaveRequest(wsio, data) {
/* data includes
data = {
app: Name of application,
id: id of application,
asset: true,
filePath: {
subdir: subdirectory app wishes file to be saved in
name: name of the file
ext: file extension
},
saveData: file data
}
*/
if (data.filePath) {
var appFileSaveDirectory, appdir;
// is it an asset or an application file
if (data.asset) {
// save in the user's folder (~/Documents/SAGE2_Media)
appFileSaveDirectory = path.join(mediaFolders.user.path, "tmp");
appdir = appFileSaveDirectory;
} else {
// save in protecteed application folder
appFileSaveDirectory = path.join(mediaFolders.user.path, "savedFiles");
appdir = path.join(appFileSaveDirectory, data.app);
}
// Take the filename
var filename = data.filePath.name;
if (filename.indexOf("." + data.filePath.ext) === -1) {
// add extension if it is not present in name
filename += "." + data.filePath.ext;
}
// save the file in the specific application folder
var filedir = appdir;
if (data.filePath.subdir) {
// add a sub-directory if asked
filedir = path.join(appdir, data.filePath.subdir);
}
// check and create the folder if needed
if (!sageutils.folderExists(filedir)) {
sageutils.mkdirParent(filedir);
}
// finally, build the full path
var fullpath = path.join(filedir, filename);
// and write the file
try {
fs.writeFileSync(fullpath, data.saveData);
sageutils.log('File', "saved file to", fullpath);
if (data.asset) {
var fileObject = {};
fileObject[0] = {
name: filename,
type: data.filePath.ext,
path: fullpath};
// Add the file to the asset library and open it
manageUploadedFiles(fileObject, [0, 0], data.app, "#B4B4B4", true);
}
} catch (err) {
sageutils.log('File', "error while saving to", fullpath + ":" + err);
}
} else {
sageutils.log('File', "file directory not specified. File not saved.");
}
}
function wsRequestFileBuffer(wsio, data) {
if (data.createdOn === null || data.createdOn === undefined) {
data.createdOn = Date.now();
}
var app = SAGE2Items.applications.list[data.id];
if (fileBufferManager.hasFileBufferForApp(data.id) === true) {
fileBufferManager.editCredentialsForBuffer({appId: data.id, owner: data.owner, createdOn: data.createdOn});
} else {
console.log("Creating file buffer for:", app.application);
fileBufferManager.requestBuffer({appId: data.id, owner: data.owner, createdOn: data.createdOn,
color: data.color, content: data.content});
}
if (data.fileName !== null && data.fileName !== undefined) {
// Create the folder as needed
var fileSaveDir = path.join(mainFolder.path, "notes");
fileSaveDir = path.join(fileSaveDir, app.application);
// Take the filename
var filename = data.fileName;
var ext = data.extension || "txt";
if (filename.indexOf("." + ext) === -1) {
// add extension if it is not present in name
filename += "." + ext;
}
// save the file in the specific application folder
if (data.subdir) {
// add a sub-directory if asked
fileSaveDir = path.join(fileSaveDir, data.subdir);
}
fileBufferManager.associateFile({appId: data.id, appName: app.application, fileDir: fileSaveDir, fileName: filename});
}
}
function wsCloseFileBuffer(wsio, data) {
console.log("Closing buffer for:", data.id);
fileBufferManager.closeFileBuffer(data.id);
}
function wsUpdateFileBufferCursorPosition(wsio, data) {
fileBufferManager.updateFileBufferCursorPosition(data);
}
function wsRequestNewTitle(wsio, data) {
var app = SAGE2Items.applications.list[data.id];
if (app !== null && app !== undefined) {
app.title = data.title;
broadcast('setTitle', data);
}
}
/**
* Create a new screen partition with dimensions specified in data
*
* @method wsCreatePartition
* @param {object} data - The dimensions of the partition to be created
*/
function wsCreatePartition(wsio, data) {
// Create Test partition
sageutils.log('Partition', "Creating a new partition");
var newPtn = createPartition(data, "#ffffff");
// update the title of the new partition
broadcast('partitionWindowTitleUpdate', newPtn.getTitle());
}
/**
* Create a new screen partition with dimensions specified in data
*
* @method wsPartitionScreen
* @param {object} data - Contains the layout specificiation with which partitions will be created
*/
function wsPartitionScreen(wsio, data) {
sageutils.log('Partition', "Dividing SAGE2 into partitions");
partitions.unusedColors = partitions.defaultColors.slice(0, partitions.defaultColors.length);
divideAreaPartitions(
data,
0,
config.ui.titleBarHeight,
config.totalWidth,
config.totalHeight - config.ui.titleBarHeight
);
delete partitions.unusedColors;
}
function divideAreaPartitions(data, x, y, width, height) {
let currX = x,
currY = y;
// if we are out of unused colors, reset the list
if (partitions.unusedColors.length === 0) {
partitions.unusedColors = partitions.defaultColors.slice(0, partitions.defaultColors.length);
}
let randIndex = Math.floor(Math.random() * partitions.unusedColors.length);
let randColor = partitions.unusedColors[randIndex];
// delete the random color from the unused colors
partitions.unusedColors.splice(randIndex, 1);
if (data.ptn) {
let newPtn = createPartition(
{
left: x,
top: y,
width: width,
height: height - config.ui.titleBarHeight,
isSnapping: true
},
randColor
);
broadcast('partitionWindowTitleUpdate', newPtn.getTitle());
} else {
if (data.type === "col") {
for (let i = 0; i < data.children.length; i++) {
divideAreaPartitions(
data.children[i],
currX,
currY,
width,
height * data.children[i].size / 12
);
currY += height * data.children[i].size / 12;
}
} else if (data.type === "row") {
for (let i = 0; i < data.children.length; i++) {
divideAreaPartitions(
data.children[i],
currX,
currY,
width * data.children[i].size / 12,
height
);
currX += width * data.children[i].size / 12;
}
}
}
}
/**
* Remove all partitions
*
* @method wsDeleteAllPartitions
*/
function wsDeleteAllPartitions(wsio) {
deleteAllPartitions();
}
/**
* Cause all apps to be associated with a partition if it is above one
* (WebSocket method)
*
* @method wsPartitionsGrabAllContent
*/
function wsPartitionsGrabAllContent(wsio) {
// associate any existing apps with partitions
partitionsGrabAllContent();
}
/**
* Cause all apps to be associated with a partition if it is above one
*
* @method partitionsGrabAllContent
*/
function partitionsGrabAllContent() {
// associate any existing apps with partitions
for (var key in SAGE2Items.applications.list) {
var changedPartitions = partitions.updateOnItemRelease(SAGE2Items.applications.list[key]);
changedPartitions.forEach((id => {
updatePartitionInnerLayout(partitions.list[id], true);
broadcast('partitionWindowTitleUpdate', partitions.list[id].getTitle());
}));
}
}
/**
* Create a new partition with a given set of dimensions and a color
*
* @method createPartition
* @param {object} dims - The dimensions of a partition in top, left, width, height
* @param {string} color - The color of the partition
*/
function createPartition(dims, color) {
var myPtn = partitions.newPartition(dims, interactMgr, color);
broadcast('createPartitionWindow', myPtn.getDisplayInfo());
broadcast('createPartitionBorder', myPtn.getDisplayInfo());
// on creation, if it is snapping, update the neighbors
if (myPtn.isSnapping) {
partitions.updateNeighbors(myPtn.id);
broadcast('updatePartitionSnapping', myPtn.getDisplayInfo());
for (let p of Object.keys(myPtn.neighbors)) {
if (partitions.list[p]) {
broadcast('updatePartitionSnapping', partitions.list[p].getDisplayInfo());
}
}
}
return myPtn;
}
/**
* Create a new partition with a given set of dimensions and a color
*
* @method createPartition
* @param {string} id - The id of the partition to be deleted
*/
function deletePartition(id) {
var ptn = partitions.list[id];
if (ptn.isSnapping) {
// remove itself from neighbors' neighbor lists
for (let neigh of Object.keys(ptn.neighbors)) {
delete partitions.list[neigh].neighbors[id];
}
}
broadcast('deletePartitionWindow', ptn.getDisplayInfo());
partitions.removePartition(ptn.id);
interactMgr.removeGeometry(ptn.id, "partitions");
}
/**
* Will attempt to take a transcript and use best case to activate a context menu item.
* All logic is performed within the src/voiceToAction.js file.
*
* @method wsVoiceToAction
* @param {Object} wsio - ws to originator.
* @param {Object} data - should contain words.
*/
function wsVoiceToAction(wsio, data) {
voiceHandler.process(wsio, data);
}
/**
* Resend display hardware information to performance page
*
*/
function wsRequestClientUpdate(wsio) {
performanceManager.updateClient(wsio);
}