API Docs for: 2.0.0

public/src/SAGE2_runtime.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-16

/* global hostAlias */

"use strict";

/**
 * Generic functions used by all SAGE2 applications
 *
 * @module client
 * @submodule SAGE2_runtime
 * @class SAGE2_runtime
 */


/**
 * Global object, containing the version number
 *
 * @property __SAGE2__
 * @type {Object}
 */
var __SAGE2__ = {};
__SAGE2__.version = "3.0.0";


/**
 * In Strict mode Webix doesn't use "eval"
 * Should be enabled if Content Security Policy is switched on for the application
 * or if the application runs in a "strict" mode
 * The flag should be enabled before Webix files are included into the page
 */
window.webix_strict = true;

/**
 * Initializes global settings: random generator, ...
 *
 * @method SAGE2_initialize
 * @param data_seed {Date} seed number
 */
function SAGE2_initialize(data_seed) {
	// Reset random number based on server's time
	Math.seed(data_seed.getTime());
}

/**
 * Detect the current browser
 *
 * @method SAGE2_browser
 */
function SAGE2_browser() {
	var browser = {};
	var userAgent = window.navigator.userAgent.toLowerCase();
	// Internet Explorer 6-11
	browser.isIE       = /*@cc_on!@*/false || !!document.documentMode;
	// Edge 20+
	browser.isEdge     = !browser.isIE && !!window.StyleMedia;
	browser.isOpera    = userAgent.indexOf("opr") >= 0;
	browser.isChrome   = !browser.isIE && userAgent.indexOf("chrome") >= 0;
	browser.isWebKit   = userAgent.indexOf("webkit") >= 0;
	browser.isSafari   = !browser.isChrome && !browser.isIE && userAgent.indexOf("safari") >= 0;
	browser.isGecko    = !browser.isWebKit && userAgent.indexOf("gecko") >= 0;
	browser.isFirefox  = browser.isGecko && userAgent.indexOf("firefox") >= 0;
	browser.isWinPhone = userAgent.indexOf("windows phone") >= 0;
	browser.isIPhone   = userAgent.indexOf("iphone") >= 0;
	browser.isIPad     = userAgent.indexOf("ipad") >= 0;
	browser.isIPod     = userAgent.indexOf("ipod") >= 0;
	browser.isIOS      = !browser.isWinPhone && (browser.isIPhone || browser.isIPad || browser.isIPod);
	browser.isAndroid  = userAgent.indexOf("android") >= 0;
	browser.isAndroidTablet = (userAgent.indexOf("android") >= 0) && !(userAgent.indexOf("mobile") >= 0);
	browser.isWindows  = userAgent.indexOf("windows") >= 0 || userAgent.indexOf("win32") >= 0;
	browser.isMac      = !browser.isIOS && (userAgent.indexOf("macintosh") >= 0 || userAgent.indexOf("mac os x") >= 0);
	browser.isLinux    = userAgent.indexOf("linux") >= 0;
	browser.isElectron = (typeof window !== 'undefined' && window.process && window.process.type === "renderer") === true;
	// Mobile clients
	browser.isMobile   = browser.isWinPhone || browser.isIOS || browser.isAndroid;

	// Store a string for the type of browser
	var browserType = browser.isElectron ? "Electron" :
		browser.isIE ? "Explorer" :
			browser.isEdge ? "Edge" :
				browser.isFirefox ? "Firefox" :
					browser.isSafari ? "Safari" :
						browser.isOpera ? "Opera" :
							browser.isChrome ? "Chrome" : "---";
	browser.browserType  = browserType;

	// Detecting version
	var _browser = {};
	var match    = '';
	var ua       = userAgent;
	_browser.electron = /electron/.test(ua);
	_browser.opr      = /mozilla/.test(ua) && /applewebkit/.test(ua) && /chrome/.test(ua) && /safari/.test(ua) && /opr/.test(ua);
	_browser.firefox  = /mozilla/.test(ua) && /firefox/.test(ua);
	_browser.msie     = /msie/.test(ua) || /trident/.test(ua) || /edge/.test(ua);
	_browser.safari   = /safari/.test(ua)  && /applewebkit/.test(ua) && !/chrome/.test(ua);
	_browser.chrome   = /webkit/.test(ua) && /chrome/.test(ua) && !/edge/.test(ua);
	_browser.version  = '';
	for (var x in _browser) {
		if (_browser[x]) {
			match = ua.match(
				new RegExp("(" + (x === "msie" ? "msie|edge" : x === "safari" ? "version" : x) + ")( |/)([0-9.]+)")
			);
			if (match) {
				_browser.version = match[3];
			} else {
				match = ua.match(new RegExp("rv:([0-9]+)"));
				_browser.version = match ? match[1] : "";
			}
			break;
		}
	}
	browser.version = _browser.version;
	// version done

	// Keep a copy of the UA
	browser.userAgent  = userAgent;
	// Copy into the global object
	__SAGE2__.browser   = browser;
}

/**
 * Debug log function: send parameters to server for printout
 *   if mutiple paramters, sent as one array
 *
 * @method log
 * @param obj {Object} data to be printed
 */
function log(obj) {
	if (arguments.length === 0) {
		return;
	}
	var args;
	if (arguments.length > 1) {
		args = Array.prototype.slice.call(arguments);
	} else {
		args = obj;
	}
	// send a log message to the server
	sage2Log({app: "client", message: args});
}

/**
 * Basic function for creating a canvas in the DOM
 *
 * @method createDrawingElement
 * @param id {String} DOM id to be created
 * @param className {String}  CSS class
 * @param posx {Number} position x
 * @param posy {Number}  position y
 * @param width {Number}  width (number in pixel)
 * @param height {Number} height (number in pixel)
 * @param depth {Number} z index
 * @return {Element} DOM canvas element
 */
function createDrawingElement(id, className, posx, posy, width, height, depth) {
	var element = document.createElement("canvas");
	element.id  = id;
	element.className    = className;
	element.width        = width;
	element.height       = height;
	element.style.left   = posx.toString() + "px";
	element.style.top    = posy.toString() + "px";
	element.style.zIndex = depth.toString();
	return element;
}


/**
 * Basic data types for inter-application communications
 *
 * @property SAGE2types
 * @type {Object}
 */
var SAGE2types = {};
function _LatLng(lat, lng) {
	this.description = "Depicts a geolocation";
	this.value   = {lat: lat, lng: lng};
	this.jsonstr = JSON.stringify(this.value);
}
SAGE2types.LatLng = _LatLng;

function _Int(val) {
	this.description = "Depicts an integer value";
	this.value   = parseInt(val);
	this.jsonstr = JSON.stringify(this.value);
}
SAGE2types.Int = _Int;

function _Float(val) {
	this.description = "Depicts an floating point value";
	this.value   = parseFloat(val);
	this.jsonstr = JSON.stringify(this.value);
}
SAGE2types.Float = _Float;

function _String(str) {
	this.description = "Depicts a string of characters";
	this.value   = str;
	this.jsonstr = str;
}
SAGE2types.String = _String;

function _Boolean(val) {
	this.description = "Depicts a boolean value";
	this.value   = (val === true);
	this.jsonstr = JSON.stringify(val);
}
SAGE2types.Boolean = _Boolean;

function _Object(obj) {
	this.description = "Depicts a javascript object";
	if (_typeOf(obj) === 'object') {
		this.value = obj;
	} else {
		this.value = {value: obj};
	}
	this.jsonstr = JSON.stringify(this.value);
}
SAGE2types.Object = _Object;

function _Array(obj) {
	this.description = "Depicts a javascript array";
	if (_typeOf(obj) === 'array') {
		this.value = obj;
	} else {
		this.value = [obj];
	}
	this.jsonstr = JSON.stringify(this.value);
}
SAGE2types.Array = _Array;

function _Date(obj) {
	this.description = "Depicts a javascript date";
	if (_typeOf(obj) === 'date') {
		this.value = obj;
	} else {
		this.value = new Date(obj);
	}
	this.jsonstr = JSON.stringify(this.value);
}
SAGE2types.Date = _Date;

SAGE2types.isaLatLng = function(obj) {
	return obj instanceof SAGE2types.LatLng;
};
SAGE2types.isaInt = function(obj) {
	return obj instanceof SAGE2types.Int;
};
SAGE2types.isaFloat = function(obj) {
	return obj instanceof SAGE2types.Float;
};
SAGE2types.isaString = function(obj) {
	return obj instanceof SAGE2types.String;
};
SAGE2types.isaObject = function(obj) {
	return obj instanceof SAGE2types.Object;
};
SAGE2types.isaArray = function(obj) {
	return obj instanceof SAGE2types.Array;
};
SAGE2types.isaDate = function(obj) {
	return obj instanceof SAGE2types.Date;
};

SAGE2types.create = function(val) {
	if (_typeOf(val) === 'object') {
		if (val.hasOwnProperty('lat') && val.hasOwnProperty('lng')) {
			return new SAGE2types.LatLng(val.lat, val.lng);
		}
		return new SAGE2types.Object(val);
	}
	if (_typeOf(val) === 'array') {
		return new SAGE2types.Array(val);
	}
	if (_typeOf(val) === 'number') {
		var v = parseInt(val);
		if (v === val) {
			return new SAGE2types.Int(val);
		}
		return new SAGE2types.Float(val);
	}
	if (_typeOf(val) === 'string') {
		return new SAGE2types.String(val);
	}
	if (_typeOf(val) === 'date') {
		return new SAGE2types.Date(val);
	}
	return null;
};

// from javascript.crockford.com
function _typeOf(value) {
	var s = typeof value;
	if (s === 'object') {
		if (value) {
			if (value instanceof Array) {
				s = 'array';
			} else if (value instanceof Date) {
				s = 'date';
			}
		} else {
			s = 'null';
		}
	}
	return s;
}

/**
 * Pretty print in browser and send to server
 *
 * @method sage2Log
 * @param msgObject {Object} data to be printed
 */
function sage2Log(msgObject) {
	// Local console print
	console.log("%c[%s] %c%s", "color: cyan;", msgObject.app,
		"color: grey;", JSON.stringify(msgObject.message));

	// Add the display node ID to the message
	msgObject.node = clientID;

	// Send the message to the server
	wsio.emit('sage2Log', msgObject);
}

/**
 * Send the broadcast call to the server
 *
 * @method broadcast
 * @param dataObject {Object} data to be sent
 */
function broadcast(dataObject) {
	wsio.emit('broadcast', dataObject);
}

/**
 * Pretty print a date object into string
 *
 * @method formatAMPM
 * @param date {Object} momentjs object for time
 * @return {String} formatted date
 */
function formatAMPM(date) {
	return date.format('h:mm a');
}

/**
 * Convert date into 24h string format
 *
 * @method format24Hr
 * @param date {Object} momentjs object for time
 * @return {String} formatted date
 */
function format24Hr(date) {
	return date.format('HH:mm');
}

/**
 * Convert a duration number into readable string
 *
 * @method formatHHMMSS
 * @param duration {Number} duration
 * @return {String} formatted duration
 */
function formatHHMMSS(duration) {
	var ss = parseInt((duration / 1000) % 60,             10);
	var mm = parseInt((duration / (1000 * 60)) % 60,      10);
	var hh = parseInt((duration / (1000 * 60 * 60)) % 24, 10);

	hh = (hh < 10) ? "0" + hh : hh;
	mm = (mm < 10) ? "0" + mm : mm;
	ss = (ss < 10) ? "0" + ss : ss;

	return (hh + ":" + mm + ":" + ss);
}

/**
 * Decode base64 into string
 *
 * @method base64ToString
 * @param base64 {String} content to be decoded
 * @return {String} string content
 */
function base64ToString(base64) {
	return atob(base64);
}

/**
 * Encode a string to base64
 *
 * @method stringToBase64
 * @param string {String} content to be encoded
 * @return {String} base64 content
 */
function stringToBase64(string) {
	return btoa(string);
}

/**
 * Smallest transparent image, put in a image tag source
 *
 * @method smallTansparentGIF
 * @return {String} small GIF image in base64 content
 */
function smallTansparentGIF() {
	return "data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==";
}

/**
 * Smallest white image, put in a image tag source
 *
 * @method smallWhiteGIF
 * @return {String} small GIF image in base64 content
 */
function smallWhiteGIF() {
	return "data:image/gif;base64,R0lGODlhAQABAIABAP///wAAACwAAAAAAQABAAACAkQBADs=";
}


/**
 * Convert a string to Uint8Array typed array
 *
 * @method stringToUint8Array
 * @param string {String} string to be converted
 * @return {TypedArray} resulting array
 */
function stringToUint8Array(string) {
	var uint8Array = new Uint8Array(new ArrayBuffer(string.length));
	for (var i = 0; i < string.length; i++) {
		uint8Array[i] = string.charCodeAt(i);
	}
	return uint8Array;
}

/**
 * Convert a string to Uint8Array typed array
 *
 * @method base64ToUint8Array
 * @param base64 {String} string to be converted
 * @return {TypedArray} resulting array
 */
function base64ToUint8Array(base64) {
	// This is a native function that decodes a base64-encoded string
	var raw = atob(base64);
	var uint8Array = new Uint8Array(new ArrayBuffer(raw.length));
	for (var i = 0; i < raw.length; i++) {
		uint8Array[i] = raw.charCodeAt(i);
	}
	return uint8Array;
}

/**
 * Do a HTTP GET request to the server to retrieve a file content
 *
 * @method readFile
 * @param filename {String} URL of the file to be read
 * @param callback {Function} called when data is received: callback(err, data)
 * @param type {String} type of data: TEXT, JSON, CSV, or SVG
 */
function readFile(filename, callback, type) {
	var dataType = type || "TEXT";

	var xhr = new XMLHttpRequest();
	xhr.open("GET", filename, true);
	xhr.onreadystatechange = function() {
		if (xhr.readyState === 4) {
			if (xhr.status === 200) {
				if (dataType === "TEXT") {
					callback(null, xhr.responseText);
				} else if (dataType === "JSON") {
					callback(null, JSON.parse(xhr.responseText));
				} else if (dataType === "CSV") {
					callback(null, csvToArray(xhr.responseText));
				} else if (dataType === "SVG") {
					callback(null, xhr.responseXML.getElementsByTagName('svg')[0]);
				} else {
					callback(null, xhr.responseText);
				}
			} else {
				callback("Error: File Not Found", null);
			}
		}
	};
	xhr.send();
}

/**
 * Convert CSV data to an array (used by the readFile function)
 *
 * @method csvToArray
 * @param strData {String} data to be converter
 * @param strDelimiter {String} string delimter (parsing using RegExp)
 * @return {Array} array containing the parsed data
 */
function csvToArray(strData, strDelimiter) {
	// Check to see if the delimiter is defined. If not,
	// then default to comma.
	strDelimiter = strDelimiter || ",";

	// Create a regular expression to parse the CSV values.
	var objPattern = new RegExp(("(\\" + strDelimiter +
		"|\\r?\\n|\\r|^)" + "(?:\"([^\"]*(?:\"\"[^\"]*)*)\"|" + "([^\"\\" +
		strDelimiter + "\\r\\n]*))"), "gi");

	// Create an array to hold our data. Give the array
	// a default empty first row.
	var arrData = [[]];

	// Create an array to hold our individual pattern
	// matching groups.
	var arrMatches = null;

	// Keep looping over the regular expression matches
	// until we can no longer find a match.
	while (arrMatches = objPattern.exec(strData)) {  // eslint-disable-line

		// Get the delimiter that was found.
		var strMatchedDelimiter = arrMatches[ 1 ];

		// Check to see if the given delimiter has a length
		// (is not the start of string) and if it matches
		// field delimiter. If id does not, then we know
		// that this delimiter is a row delimiter.
		if (strMatchedDelimiter.length && strMatchedDelimiter !== strDelimiter) {
			// Since we have reached a new row of data,
			// add an empty row to our data array.
			arrData.push([]);
		}

		var strMatchedValue;

		// Now that we have our delimiter out of the way,
		// let's check to see which kind of value we
		// captured (quoted or unquoted).
		if (arrMatches[2]) {
			// We found a quoted value. When we capture
			// this value, unescape any double quotes.
			strMatchedValue = arrMatches[2].replace(new RegExp("\"\"", "g"), "\"");
		} else {
			// We found a non-quoted value.
			strMatchedValue = arrMatches[3];

		}

		// Now that we have our value string, let's add
		// it to the data array.
		arrData[arrData.length - 1].push(strMatchedValue);
	}

	// Return the parsed data.
	return arrData;
}

/**
 * Calculate the average value of an array
 *
 * @method average
 * @param arr {Array} values to be averaged
 * @return {Number} the average
 */
function average(arr) {
	var l = arr.length;
	if (l === 0) {
		return 0;
	}
	var sum = 0;
	for (var i = 0; i < l; i++) {
		sum += arr[i];
	}
	return sum / l;
}

/**
 * Test if all values of an object are true
 *
 * @method allTrueDict
 * @param dict {Object} values
 * @return {Bool} true if all values are true
 */
function allTrueDict(dict) {
	var key;
	for (key in dict) {
		if (dict[key] !== true) {
			return false;
		}
	}
	return true;
}

/**
 * Extract the parameter value from the current URL (?clientID=0&param=4)
 *
 * @method getParameterByName
 * @param name {String} parameter to search for
 * @return {String} null or the value found
 */
function getParameterByName(name) {
	name = name.replace(/[\[]/, "\\\[").replace(/[\]]/, "\\\]"); // eslint-disable-line
	var regex = new RegExp("[\\?&]" + name + "=([^&#]*)");
	var results = regex.exec(location.search);
	return results === null ? "" : decodeURIComponent(results[1].replace(/\+/g, " "));
}

/**
 * Search for a video tag and switch its state
 *
 * @method playPauseVideo
 * @param elemId {String} video element to search
 */
function playPauseVideo(elemId) {
	var videoElem = document.getElementById(elemId + "_video");
	if (videoElem.paused === true) {
		videoElem.play();
	} else {
		videoElem.pause();
	}
}

/**
 * Reorder elements at given level in the DOM
 *
 * @method moveItemToFront
 * @param elem {Element} DOM element to reorder
 */
function moveItemToFront(elem) {
	var last = elem.parentNode.lastChild;
	if (elem !== last) {
		elem.parentNode.replaceChild(elem, last);
		elem.parentNode.insertBefore(last, elem);
	}
}

/**
 * Delete an element in the DOM
 *
 * @method deleteElement
 * @param id {Element} DOM element to delete
 */
function deleteElement(id) {
	var elem = document.getElementById(id);
	if (elem !== undefined && elem !== null) {
		elem.parentNode.removeChild(elem);
	}
}

/**
 * Remove of children of a DOM element
 *
 * @method removeAllChildren
 * @param node {Element|String} id or node to be processed
 */
function removeAllChildren(node) {
	// if the parameter a string, look it up
	var elt = (typeof node === "string") ? document.getElementById(node) : node;
	// remove one child at a time
	while (elt.lastChild) {
		elt.removeChild(elt.lastChild);
	}
}


/**
 * Cleanup a URL and replace the origin to match the client (to mitigate CORS problems, cross-origin resource sharing)
 *
 * @method cleanURL
 * @param url {String} URL to clenaup
 * @return {Strinf} new URL
 */
function cleanURL(url) {
	if (url === null) {
		return url;
	}
	var a = document.createElement('a');
	a.href = url;
	var clean = url;

	if (hostAlias[a.origin] !== undefined) {
		clean = url.replace(a.origin, hostAlias[a.origin]);
	}

	return clean;
}

/**
 *
 */
function ignoreFields(obj, fields) {
	var key;

	var result = {};
	for (key in obj) {
		if (fields.indexOf(key) < 0) {
			if (obj[key] === null || obj[key] instanceof Array || typeof obj[key] !== "object") {
				result[key] = obj[key];
			} else {
				result[key] = ignoreFields(obj[key], fields);
			}
		}
	}
	if (isEmpty(result)) {
		return undefined;
	}
	return result;
}

/**
 * Utility function to test if a string or number represents a true value.
 * Used for parsing JSON values
 *
 * @method parseBool
 * @param value {Object} value to test
 */
function parseBool(value) {
	if (typeof value === 'string') {
		value = value.toLowerCase();
	}
	switch (value) {
		case true:
		case "true":
		case 1:
		case "1":
		case "on":
		case "yes": {
			return true;
		}
		default: {
			return false;
		}
	}
}

/**
 * Test if element is equal to true (used in .every call on an array)
 *
 * @method isTrue
 * @param element {Bool} The current element being processed in the array
 * @param index {Number} The index of the current element being processed in the array
 * @param array {Array} The array every was called upon
 * @return {Bool} true if element is true
 */
function isTrue(element, index, array) {
	return (element === true);
}

/**
 * Test is an object is equivalen to 'empty'
 *
 * @method isEmpty
 * @param obj {Object} value to be tested
 * @return {Bool} true if empty
 */
function isEmpty(obj) {
	// undefined and null are "empty"
	if (obj === undefined || obj === null) {
		return true;
	}

	// Assume if it has a length property with a non-zero value
	// that that property is correct.
	if (obj.length > 0) {
		return false;
	}
	if (obj.length === 0) {
		return true;
	}

	// Otherwise, does it have any properties of its own?
	// Note that this doesn't handle
	// toString and valueOf enumeration bugs in IE < 9
	for (var key in obj) {
		if (hasOwnProperty.call(obj, key)) {
			return false;
		}
	}
	return true;
}

/**
 * Create an array of given size filled with a value
 *
 * @method initializeArray
 * @param size {Number} size of the array
 * @param value {Value} initial value
 * @return {Array} new array
 */
function initializeArray(size, value) {
	var arr = new Array(size);
	for (var i = 0; i < size; i++) {
		arr[i] = value;
	}
	return arr;
}

/**
 * Convert an array of byte to an integer
 *
 * @method byteBufferToInt
 * @param buff {Array} buffer to be processed
 * @return {Number} resulting value
 */
function byteBufferToInt(buf) {
	var value = 0;
	for (var i = buf.length - 1; i >= 0; i--) {
		value = (value * 256) + buf[i];
	}
	return value;
}

/**
 * Convert an array of byte to a string (using fromCharCode on every character)
 *
 * @method byteBufferToString
 * @param buff {Array} buffer to be processed
 * @return {String} string
 */
function byteBufferToString(buf) {
	var str = "";
	var i   = 0;
	while (buf[i] !== 0 && i < buf.length) {
		str += String.fromCharCode(buf[i]);
		i++;
	}
	return str;
}

/**
 * Overload Math.seed and Math.random to be deterministic, for distributed work
 *
 * @method Math.seed
 * @param s {Number} seed
 */
Math.seed = function(s) {
	Math.random = function() {
		// POSIX drand48 ==> Xn+1 = (a*Xn+c) % m
		var a = 25214903917;
		var c = 11;
		var m = 281474976710656;
		s = (a * s + c) % m;
		return s / m;
	};
};

/**
 * Add a key-value pair as a cookie
 *
 * @method addCookie
 * @param sKey {String} key
 * @param sValue {String} value
 * @return {Boolean} true/false
 */
function addCookie(sKey, sValue) {
	if (!sKey) {
		return false;
	}
	var domain;
	if (window.location.hostname === "127.0.0.1") {
		domain = "127.0.0.1";
	} else {
		var domainPieces = window.location.hostname.split('.');
		var maybeInt = parseInt(domainPieces[domainPieces.length - 1]);
		var numberOfPiecesFromEndTokeep;

		// if (maybeInt) { // this is a number, so must be last part of an ip address, need 4 parts
		// 	numberOfPiecesFromEndTokeep = 4;
		// } else if (domainPieces[domainPieces.length - 1] == "tw") {
		// 	numberOfPiecesFromEndTokeep = 3;
		// } else { // was a hostname extension
		// 	numberOfPiecesFromEndTokeep = 2;
		// }

		// NaN triggers false on a test
		if (maybeInt) {
			// this is a number, so must be last part of an ip address
			// use the whole hostname
			numberOfPiecesFromEndTokeep = domainPieces.length;
		} else {
			// was a hostname extension
			// to get domain, remove hostname
			numberOfPiecesFromEndTokeep = domainPieces.length - 1;
		}

		// calculate the domain from the spliced hostname
		domain = domainPieces.slice(-1 * numberOfPiecesFromEndTokeep).join(".");
	}

	document.cookie = encodeURIComponent(sKey) + "=" + encodeURIComponent(sValue) +
		"; expires=Fri, 31 Dec 9999 23:59:59 GMT" +
		"; domain=" + domain +
		"; path=/";
	return true;
}

/**
 * Delete a cookie for a given key
 *
 * @method deleteCookie
 * @param sKey {String} key
 * @return {Boolean} true/false
 */
function deleteCookie(sKey) {
	if (!sKey) {
		return false;
	}
	document.cookie = encodeURIComponent(sKey) + '=; expires=Thu, 01 Jan 1970 00:00:01 GMT;';
	return true;
}

/**
 * Return a cookie value for given key
 *
 * @method getCookie
 * @param sKey {String} key
 * @return {String} value found or null
 */
function getCookie(sKey) {
	if (!sKey) {
		return null;
	}
	return decodeURIComponent(document.cookie.replace(new RegExp("(?:(?:^|.*;)\\s*" +
		encodeURIComponent(sKey).replace(/[-.+*]/g, "\\$&") + "\\s*\\=\\s*([^;]*).*$)|^.*$"), "$1"))
		|| null;
}

/**
 * Extract translate and scale from a DOM element
 *
 * @method getTransform
 * @param elem {Object} DOM element
 * @return {Object} contains translate and scale specification
 */
function getTransform(elem) {
	var transform = elem.style.transform;
	var translate = {x: 0, y: 0};
	var scale = {x: 1, y: 1};
	if (transform) {
		var tIdx = transform.indexOf("translate");
		if (tIdx >= 0) {
			var tStr = transform.substring(tIdx + 10, transform.length);
			tStr = tStr.substring(0, tStr.indexOf(")"));
			var tValue = tStr.split(",");
			translate.x = parseFloat(tValue[0]);
			translate.y = parseFloat(tValue[1]);
		}
		var sIdx = transform.indexOf("scale");
		if (sIdx >= 0) {
			var sStr = transform.substring(sIdx + 6, transform.length);
			sStr = sStr.substring(0, sStr.indexOf(")"));
			var sValue = sStr.split(",");
			scale.x = parseFloat(sValue[0]);
			scale.y = parseFloat(sValue[1]);
		}
	}
	return {translate: translate, scale: scale};
}


/**
 * From stackoverflow:
 * Copies a string to the clipboard. Must be called from within an
 * event handler such as click. May return false if it failed, but
 * this is not always possible. Browser support for Chrome 43+,
 * Firefox 42+, Safari 10+, Edge and IE 10+.
 * IE: The clipboard feature may be disabled by an administrator. By
 * default a prompt is shown the first time the clipboard is
 * used (per session)
 *
 * @method     copyToClipboard
 * @param      {String}   text    The text to be copied
 * @return     {Boolean}  A Boolean that is false if the command is not supported or enabled
 */
function SAGE2_copyToClipboard(text) {
	if (window.clipboardData && window.clipboardData.setData) {
		// IE specific code path to prevent textarea being shown while dialog is visible.
		return window.clipboardData.setData("Text", text);
	} else if (document.queryCommandSupported && document.queryCommandSupported("copy")) {
		var textarea = document.createElement("textarea");
		textarea.textContent = text;
		// Prevent scrolling to bottom of page in MS Edge
		textarea.style.position = "fixed";
		document.body.appendChild(textarea);
		textarea.select();
		try {
			// Security exception may be thrown by some browsers
			return document.execCommand("copy");
		} catch (ex) {
			console.warn("Copy to clipboard failed.", ex);
			return false;
		} finally {
			document.body.removeChild(textarea);
		}
	}
}