/**********************************************************************************************
* 
* Javascript for 'webThrottle.html'
* 
* This script defines the web throttle behaviour.
* 
* >>> This file version: 2.4 - by Oscar Moutinho (oscar.moutinho@gmail.com)
* 
* This script relies on 'jquery.jmriConnect.js v2.1' (read its header for dependencies).
* 
* URL parameters: (roster/panels list if no parameters)
* - 'loconame' (to open a throttle for a loco)
* - 'turnouts' (to list turnouts)
* - 'routes' (to list routes)
* - 'panelname' (to open a panel)
* - 'reset' (to remove local configuration - restore defaults)
* - 'debug' (=true -> turn it ON, =false -> turn it OFF, other values -> nothing change)
* 
**********************************************************************************************/

//+++++++++++++++++++++++++++++++++++++++++++++++++++ Global Vars and Functions

//----------------------------------------- Global vars

var log = new Logger();
var $debug = true;
var $vScrollbarWidth;
var $showScrollBar = false;
var $zIndexForSmoothAlert = 999999999;										// Max z-index = 2147483647
var $jmri = null;
var $throttleType = '';
var $rosterGroup = '';
var $isRoster;
var $frameList = [];
var $toFrame = false;
var $inFrame = false;
var $pageInFrame;
var $speedPosition = true;
var $inputParameters;
var $paramLocoName;
var $paramPanels;
var $paramPanelName;
var $rosterGroups;
var $locoList;
var $panelLoaded = false;
var $resizeCheckTimer = null;
var $resizeCheckInterval = 500;												// ms
var $viewportHeight = 0;													// Initial value to force resize
var $viewportWidth = 0;														// Initial value to force resize
var $changeBothSizes;
var $heightChanged;
var $widthChanged;
var $portrait;
var $fontSizeMin = 8;														// px
var $fontSizeMax = 48;														// px
var $defaultFontSize = 16;													// px - default
var $fontSize = $defaultFontSize;
var $fontDelta;
var $fontChanged;
var $sizeCtrlPercent;
var $nextBlockTop;
var $headerHeightRef = 40;													// px
var $buttonsHeightRef = 30;													// px
var $cellHeightRef = 100;													// px
var $cellWidthRef = 500;													// px
var $speedWidthRef = 80;													// px
var $functionHeightRef = 80;												// px
var $functionWidthRef = 250;												// px
var $buttonDelayTimeout = 1000;												// ms
var $powerDelayTimer = null;
var $removeDelayTimer = null;
var $speedTimer = null;
var $speedInterval = 250;													// ms
var $speedStep = 0.10;
var $speedFeedback = true;
var $speedAux = 0;
var $hasTouch = ('ontouchstart' in window);
var $hasMovement = (window.orientation !== null) && $hasTouch;
var $orientation = null;
var $movementTilt = null;
var $movementActive = false;
var $movementOn = false;
var $movementCtrl = 0;
var $locoAddress = "none";
var $help = [];

//----------------------------------------- Generic onError
window.onerror = function(errMsg, errUrl, errLineNumber) {
	if ($jmri) $jmri.closeSocket();
	if ($resizeCheckTimer) {
		clearInterval($resizeCheckTimer);
		$resizeCheckTimer = null;
	}
	if ($speedTimer) {
		clearInterval($speedTimer);
		$speedTimer = null;
	}
	if (errMsg.indexOf('private~') >= 0) alert(errMsg.split('~')[1]);
	else {
		if (errMsg == 'Uncaught ReferenceError: stopme is not defined') location.reload(true);	// I don't know what this is !?!?!? Just reload.
		else alert('\nError running javascript:\n' + errMsg + '\n\nURL:\n' + errUrl + '\n\nLine Number: ' + errLineNumber);
	}
	document.body.innerHTML = '';
	return true;
};

//----------------------------------------- Page exit cleanup 1
window.onbeforeunload = function() {
};

//----------------------------------------- Page exit cleanup 2
window.onunload = function() {
	if ($jmri) {
		if ($('body').attr('locoReady') == 'true') $jmri.setJMRI('throttle', $locoAddress, {"release":null});
		$jmri.closeSocket();
	}
	if ($resizeCheckTimer) {
		clearInterval($resizeCheckTimer);
		$resizeCheckTimer = null;
	}
	if ($speedTimer) {
		clearInterval($speedTimer);
		$speedTimer = null;
	}
};

//----------------------------------------- Add trim method to string
String.prototype.trim = function () {return this.replace(/^\s+|\s+$/g,'');};

//----------------------------------------- Immediate execution
try {
	if (jQuery === undefined) throw new Error('private~jQuery not loaded.\nHTML5 and WebSockets needed.\nCheck browser compatibility.');
} catch(error) {
	throw new Error('private~jQuery not loaded.\nHTML5 and WebSockets needed.\nCheck browser compatibility.');
}

try {
	localStorage['webThrottle.test'] = '1';
	localStorage.removeItem('webThrottle.test');
} catch(error) {
	if (error.code === DOMException.QUOTA_EXCEEDED_ERR && localStorage.length === 0) throw new Error('private~Turn off Private Browsing.');
	else throw new Error('private~Local Storage not available.\nHTML5 and WebSockets needed.\nCheck browser compatibility.');
}

//----------------------------------------- Run at start up
$(document).ready(function() {
	/******* Constants available in '$jmri' object
	*	$jmri.powerUNKNOWN = 0;
	*	$jmri.powerON = 2;
	*	$jmri.powerOFF = 4;
	*	$jmri.turnoutUNDEFINED = 0;
	*	$jmri.turnoutUNKNOWN = 1;
	*	$jmri.turnoutCLOSED = 2;
	*	$jmri.turnoutTHROWN = 4;
	*	$jmri.routeDISABLED = 0;
	*	$jmri.routeUNDEFINED = 1;
	*	$jmri.routeACTIVE = 2;
	*	$jmri.routeINACTIVE = 4;
	*	$jmri.TRUE = true;
	*	$jmri.FALSE = false;
	*	$jmri.YES = 1;
	*	$jmri.NO = 0;
	*	$jmri.EMERGENCY_STOP = '-1.0';
	*	$jmri.STOP = '0.0';
	*	$jmri.FULL_SPEED = '1.0';
	******** Functions available in '$jmri' object
	* $jmri.getRoster(group)
	* . To list all: group = null or undefined
	* . Returns array of object: id, roadNumber, roadName, mfg, owner, model, dccAddress, imageFilePath, iconFilePath
	* $jmri.getRosterItem(id)
	* . Returns object: id, fileName, roadNumber, roadName, mfg, owner, model, dccAddress, comment, maxSpeed, imageFilePath, iconFilePath, URL, IsShuntingOn, f[i].lockable, f[i].functionlabel, f[i].functionImage, f[i].functionImageSelected
	* . If a function is not defined: f[i] = null 
	* $jmri.getRosterGroups()
	* . Returns array of strings: rosterGroup
	* $jmri.getObjectList(listType) {
	* . Possible values for string 'listType' (get): roster, panels, lights, reporters, sensors, turnouts, signalHeads, signalMasts, routes, memories, blocks, oblocks
	* . Returns array of objects: list
	* $jmri.closeSocket()
	* . To stop communication with JMRI (usually, before exit and before blocking code: alert(), ...)
	* $jmri.getJMRI(type, name)
	* . Possible values for string 'type' (get): light, reporter, sensor, turnout, signalHead, signalMast, route, memory, power, rosterEntry
	* $jmri.setJMRI(type, name, args)
	* . Possible values for string 'type' (set): light, reporter, sensor, turnout, signalHead, signalMast, route, memory, power
	* Special case for 'type' = 'power' > string 'name' should be null
	* Possible 'args' for 'throttle': {"throttle":throttleName,"address":dccAddress,"speed":speed,"forward":forward,"Fn":active} (0 <= n <= 28)
	* Possible 'args' for 'light': {"userName":userName,"comment":comment,"state":state}
	* Possible 'args' for 'reporter': {"userName":userName,"state":state,"comment":comment,"report":report,"lastReport":lastReport}
	* Possible 'args' for 'sensor': {"userName":userName,"comment":comment,"inverted":inverted,"state":state}
	* Possible 'args' for 'turnout': {"userName":userName,"comment":comment,"inverted":inverted,"state":state}
	* Possible 'args' for 'signalHead': {"userName":userName,"comment":comment,"lit":lit,"appearance":appearance,"held":held,"state":state,"appearanceName":appearanceName}
	* Possible 'args' for 'signalMast': {"userName":userName,"":aspect,"lit":lit,"held":held,"state":state}
	* Possible 'args' for 'route': {"userName":userName,"comment":comment,"state":state}
	* Possible 'args' for 'memory': {"userName":userName,"comment":comment,"value":value}
	* Possible 'args' for 'power': {"state":state}
	* block
	* oblock
	* >>> Other values for 'type' and new 'args' may be available
	********************************************/
	var debug = loadLocalInfo('webThrottle.debug');
	if (debug == 'true' || debug == 'false') $debug = (debug == 'true'); else saveLocalInfo('webThrottle.debug', $debug = false);
	$fontDelta = 0.14;	// hardcoded from testing
	var fS = loadLocalInfo('webThrottle.fontSize');
	if (fS && !isNaN(fS) && Number(fS) >= $fontSizeMin && Number(fS) <= $fontSizeMax) $fontSize = Number(fS); else saveLocalInfo('webThrottle.fontSize', $fontSize);
	$vScrollbarWidth = getVerticalScrollbarWidth();
	loadFrameList();
	for (var i = 0; i < $frameList.length; i++) {	// Check if this page is flagged to be displayed in a frame
		if ($frameList[i] == document.URL) {
			$inFrame = true;
			break;
		}
	}
	var speedPosition = loadLocalInfo('webThrottle.speedPosition');
	if (speedPosition == 'true' || speedPosition == 'false') $speedPosition = (speedPosition == 'true'); else saveLocalInfo('webThrottle.speedPosition', $speedPosition);
	var movementActive = loadLocalInfo('webThrottle.movementActive');
	if (movementActive == 'true' || movementActive == 'false') $movementActive = (movementActive == 'true'); else saveLocalInfo('webThrottle.movementActive', $movementActive);
	$inputParameters = getUrlParameters();
	debug = $inputParameters.debug;
	if (debug) debug = debug.toLowerCase();
	if (debug == 'true' || debug == 'false') {
		$debug = (debug == 'true');
		saveLocalInfo('webThrottle.debug', $debug);
		alert('Debug mode ' + ($debug ? 'started.' : 'stopped.'));
		window.open(document.URL.split('?')[0], '_top');	// Stop building throttle and restart without parameters
		return;
	}
	if ($inputParameters.reset !== undefined) {	// Remove all application parameters
		removeLocalInfo("webThrottle.debug");
		removeLocalInfo("webThrottle.fontSize");
		removeLocalInfo("webThrottle.speedPosition");
		removeLocalInfo("webThrottle.movementActive");
		removeLocalInfo("webThrottle.rosterGroup");
		removeLocalInfo("webThrottle.frameListSize");
		for(var i = 0; i < $frameList.length; i++) removeLocalInfo("webThrottle.frameList[" + i + "]");
		alert('Default configuration restored.');
		window.open(document.URL.split('?')[0], '_top');	// Stop building throttle and restart without parameters
		return;
	}
	if ($debug) smoothAlert('In debug mode ...', 3);
	$paramLocoName = $inputParameters.loconame;
	$paramPanelName = $inputParameters.panelname;
	if ($paramLocoName) $throttleType = 'loco';
	if (!$throttleType) if ($inputParameters.turnouts !== undefined) $throttleType = 'turnouts';
	if (!$throttleType) if ($inputParameters.routes !== undefined) $throttleType = 'routes';
	if (!$throttleType) if ($paramPanelName) $throttleType = 'panel';
	if (!$throttleType) $throttleType = 'roster';
	$pageInFrame = (window.parent.$('iframe').length > 0);
	if ($frameList.length && $throttleType == 'roster' && !$pageInFrame) {	// Has info for frames, is the master and it is the top window (build frameset)
		var body = $('body').attr('id', 'bodyFrames');
		var frame = $('<iframe>').attr('src', document.URL + '?frameFF').attr('id', encodeId(document.URL + '?frameFF')).addClass('frame');	// Added 'frameFF' to solve FF bug
		body.append(frame);
		for (var j = 0; j < $frameList.length; j++) {
			frame = $('<iframe>').attr('src', $frameList[j]).attr('id', encodeId($frameList[j])).addClass('frame');
			body.append(frame);
		}
		$resizeCheckTimer = setInterval(function() {
			checkSmoothAlertEnd();
			var h = $(window).height();
			var w = $(window).width();
			if ($viewportHeight != h) {
				$heightChanged = true;
				$viewportHeight = h;
			} else $heightChanged = false;
			if ($viewportWidth != w) {
				$widthChanged = true;
				$viewportWidth = w;
			} else $widthChanged = false;
			if ($heightChanged || $widthChanged) {
				$portrait = (w <= h);
				var totalFrames = $frameList.length + 1;
				/* Explanation for the next lines
					h / w = vRects / hRects
					totalFrames = vRects * hRects
					.
					vRects = h / w * hRects
					hRects = totalFrames / vRects
					.
					vRects = h / w * totalFrames / vRects
					.
					vRects^2 = h / w * totalFrames
				*/
				var ratio = h / w * 1.2;									// Ratio with correction to choose rectangularity
				var vRects = Math.round(Math.sqrt(ratio * totalFrames));	// Number of rectangles per column
				var hRects = Math.round(totalFrames / vRects);				// Number of rectangles per row
				if (vRects * hRects < totalFrames) {
					if (ratio > vRects / hRects) vRects++; else hRects++;
				}
				if ((vRects * hRects - totalFrames) >= ((vRects > hRects) ? hRects : vRects)) {
					if (ratio > vRects / hRects) vRects--; else hRects--;
				}
				var master = Math.floor((hRects - 1) / 2);
				var rectHeight = Math.floor(h / vRects);					//Each rectangle height
				var rectWidth = Math.floor(w / hRects);						//Each rectangle width
				var t = 0;
				var l = 0;
				var frames = $('.frame');
				var o;
				for (var i = 1; i <= master; i++) {
					o = frames.eq(i);
					o.attr('rightSide', (l + 1 > hRects / 2));
					o.css('top', t).css('left', l);
					setOuterHeight(o, rectHeight, true);
					setOuterWidth(o, rectWidth, true);
					if(l + rectWidth * 1.5 < w) l+= rectWidth; else {l = 0; t+= rectHeight;}
				}
				o = frames.eq(0);
				o.attr('rightSide', (l + 1 > hRects / 2));
				o.css('top', t).css('left', l);
				setOuterHeight(o, rectHeight, true);
				setOuterWidth(o, rectWidth, true);
				if(l + rectWidth * 1.5 < w) l+= rectWidth; else {l = 0; t+= rectHeight;}
				for (var i = master + 1; i < totalFrames; i++) {
					o = frames.eq(i);
					o.attr('rightSide', (l + 1 > hRects / 2));
					o.css('top', t).css('left', l);
					setOuterHeight(o, rectHeight, true);
					if (i < totalFrames - 1) {
						setOuterWidth(o, rectWidth, true);
						if(l + rectWidth * 1.5 < w) l+= rectWidth; else {l = 0; t+= rectHeight;}						
					} else {	// Last frame
						setOuterWidth(o, w - l, true);
					}
				}
				resizeSmoothAlerts();
			}
		}, $resizeCheckInterval);
		return;	// Stop building throttle if frameset
	}
	// Continue building throttle if not frameset
	startJMRI();
});

//----------------------------------------- Start JMRI
var startJMRI = function() {
	$jmri = $.JMRI({
		//*** Callback Functions available in '$jmri' object
		toSend: function(data) {$debug && log.log(new Date() + ' - ' + document.title + '\n' + 'JSONtoSend: ' + data);},	//Nothing to do
		fullData: function(data) {$debug && log.log(new Date() + ' - ' + document.title + '\n' + 'JSONreceived: ' + data);},	//Nothing to do
                error: function (code, message) {
                    if (code === 0)
                        jmriLostComm(message);
                    else if (code === 200)
                        smoothAlert(message, 10);
                    else
                        smoothAlert('Error: ' + code + ' - ' + message);
                },
		end: function() {jmriLostComm('The JMRI WebSocket service was turned off.\nSolve the problem and refresh web page.');},
		ready: function(jsonVersion, jmriVersion, railroadName) {jmriReady(jsonVersion, jmriVersion, railroadName);},	//When WebSocket connection established - continue next steps
		throttle: function(name, address, speed, forward, fs) {throttleState(name, address, speed, forward, fs);},
		light: function(name, userName, comment, state) {},	//Nothing to do
		reporter: function(name, userName, state, comment, report, lastReport) {},	//Nothing to do
		sensor: function(name, userName, comment, inverted, state) {},	//Nothing to do
		turnout: function(name, userName, comment, inverted, state) {layoutTurnoutState(name, userName, comment, inverted, state);},
		signalHead: function(name, userName, comment, lit, appearance, held, state, appearanceName) {},	//Nothing to do
		signalMast: function(name, userName, aspect, lit, held, state) {},	//Nothing to do
		route: function(name, userName, comment, state) {layoutRouteState(name, userName, comment, state);},
        memory: function(name, userName, comment, value) {},	//Nothing to do
        power: function(state) {layoutPowerState(state);},
    });
	if (!$jmri) throw new Error('private~Could not open JMRI WebSocket.');
};

//----------------------------------------- Lost communication with JMRI
var jmriLostComm = function(message) {
	var timer = loadLocalInfo('webThrottle.timerReload');
	if (timer && !isNaN(timer) && Number(timer) >= 0 && Number(timer) == Math.abs(timer)) timer = Number(timer); else saveLocalInfo('webThrottle.timerReload', timer = new Date().getTime());	// Miliseconds
	if (new Date().getTime() - timer < 30000) {	// Reload if less than 30s after communication lost
		smoothAlert('Communication lost.\nRestarting ...');
		location.reload(true);
	} else {
		throw new Error('private~' + message);
	}
};

//----------------------------------------- JMRI ready
var jmriReady = function(jsonVersion, jmriVersion, railroadName) {
	removeLocalInfo('webThrottle.timerReload');	// Communication OK -> Restart count for communication lost
	var body = $('body');
	var bodyFrameOuter = $('<div>');
	bodyFrameOuter.attr('id', 'bodyFrameOuter');
	body.append(bodyFrameOuter);
	var bodyFrameInner = $('<div>');
	bodyFrameInner.attr('id', 'bodyFrameInner');
	bodyFrameOuter.append(bodyFrameInner);
	if ($hasTouch) {	// to prevent 'ghosts', ...
//		body.on('touchstart', function(event) {event.preventDefault(); event.stopImmediatePropagation();});	// This blocks every touch !?
	} else {
		body.on('mousedown', function(event) {event.preventDefault(); event.stopImmediatePropagation();});
	}
	if ($hasMovement && window.DeviceOrientationEvent) window.addEventListener('deviceorientation', function(event) {deviceOrientation(event);});
	var header = $('<div>');
	header.attr('id', 'header');
	bodyFrameInner.append(header);
	if ($throttleType == 'roster' || !$pageInFrame) {	// Is master or isn't in a frame
		var power = $('<div>').addClass('button').attr('id', 'power').attr('state', -1);
		power.append($('<div>').text('o').addClass('buttonText'));
		if ($hasTouch) {
			power.on('touchstart', function(event) {powerButtonPressedTouch(event.originalEvent);});
			power.on('touchend', function(event) {powerButtonReleasedTouch(event.originalEvent);});
		} else {
			power.on('mousedown', function(event) {powerButtonPressedMouse(event);});
			power.on('mouseup', function(event) {powerButtonReleasedMouse(event);});
			power.on('mouseout', function(event) {powerButtonCanceledMouse(event);});
		}
		header.append(power);
	}
	if ($throttleType == 'roster') {	// Button to define new throttles in frame or not
		var toFrame;
		toFrame = $('<div>').addClass('button').attr('id', 'toFrame');
		toFrame.append($('<div>').text('single').addClass('buttonText'));
		toFrame.on('click', function(event) {manageNewThrottles(event)});
		header.append(toFrame);
	} else {
		if ($inFrame) {	// Button to remove itself from frame
			$help.push(
				'Press [X] button for 2s' +
				'\nto remove this frame.' +
				''
			);
			var removeFrame = $('<div>').addClass('button').attr('id', 'removeFrame');
			removeFrame.append($('<div>').text('X').addClass('buttonText'));
			if ($hasTouch) {
				removeFrame.on('touchstart', function(event) {removeButtonPressedTouch(event.originalEvent);});
				removeFrame.on('touchend', function(event) {removeButtonReleasedTouch(event.originalEvent);});
			} else {
				removeFrame.on('mousedown', function(event) {removeButtonPressedMouse(event);});
				removeFrame.on('mouseup', function(event) {removeButtonReleasedMouse(event);});
				removeFrame.on('mouseout', function(event) {removeButtonCanceledMouse(event);});
			}
			header.append(removeFrame);
		}
	}
	if ($throttleType == 'loco' && !$pageInFrame) {	// Button to select speed control position
		var speedPosition;
		speedPosition = $('<div>').addClass('button').attr('id', 'speedPosition');
		speedPosition.append($('<div>').text('xxx').addClass('buttonText'));
		speedPosition.on('click', function(event) {manageSpeedPosition(event)});
		header.append(speedPosition);
		changeSpeedPosition($speedPosition);
	}
	if ($throttleType == 'roster' || !$pageInFrame) {	// Is master or isn't in a frame
		var fontSmaller = $('<div>').addClass('button').attr('id', 'fontSmaller');
		fontSmaller.append($('<div>').text('A').addClass('buttonText'));
		fontSmaller.on('click', function(event) {fontSizeChange(event, -2);});
		header.append(fontSmaller);
		var fontLarger = $('<div>').addClass('button').attr('id', 'fontLarger');
		fontLarger.append($('<div>').text('A').addClass('buttonText'));
		fontLarger.on('click', function(event) {fontSizeChange(event, +2);});
		header.append(fontLarger);
	}
	var help = $('<div>').addClass('button').attr('id', 'help');
	help.append($('<div>').text('?').addClass('buttonText'));
	help.on('click', function(event) {helpShow(event);});
	header.append(help);
	switch ($throttleType) {
		case 'roster':
			var rg = loadLocalInfo('webThrottle.rosterGroup');
			if (rg || rg == '') $rosterGroup = rg; else saveLocalInfo('webThrottle.rosterGroup', $rosterGroup);
			$help.push(
				'Web Throttle for JMRI ' + jmriVersion + ' controlling \'' + railroadName + '\'' +
				'\n' +
				'\n URL parameters: (roster/panels list if no parameters)' +
				'\n- \'loconame\' (to open a throttle for a loco)' +
				'\n- \'turnouts\' (to list turnouts)' +
				'\n- \'routes\' (to list routes)' +
				'\n- \'panelname\' (to open a panel)' +
				'\n- \'reset\' (to restore defaults)' +
				'\n- \'debug\' (true or false -> turn debug ON/OFF)' +
				'\nEx.: .../web/webThrottle.html?loconame=A%20325' +
				'\n                  (for loco ID = \'A 325\')' +
				'\nEx.: .../web/webThrottle.html?turnouts' +
				'\n' +
				'\nCheck the header of \'webThrottle.html\' for' +
				'\ndependencies and versions.' +
				'\n' +
				'\n... by Oscar Moutinho (oscar.moutinho@gmail.com)' +
				''
			);
			$help.push(
				'Press [o] button for 2s to turn power ON/OFF.' +
				'\n' +
				'\nClick [single] / [multi] button to open\npages alone or grouped.' +
				'\n' +
				'\nAlert messages:' +
				'\n- Older messages are on top of the others;' +
				'\n- They can be moved - just drag them.' +
				'\n' +
				'\nAdvices:' +
				'\n- Prevent device sleep when running.' +
				'\n- You may want to prevent rotation on your tablet.' +
				''
			);
			$isRoster = ($rosterGroup != '[Panels]');
			var buttons = $('<div>');
			buttons.attr('id', 'buttons');
			bodyFrameInner.append(buttons);
			$rosterGroups = $jmri.getRosterGroups();
			var groups = $('<div>').addClass('button').attr('id', 'groups');
			groups.append($('<div>').text('R. Groups / Panels').addClass('buttonText'));
			groups.on('click', function(event) {chooseGroup(event);});
			buttons.append(groups);
			var turnouts = $('<div>').addClass('button').attr('id', 'turnouts');
			turnouts.append($('<div>').text('Turnouts').addClass('buttonText'));
			turnouts.on('click', function(event) {turnoutsShow(event);});
			buttons.append(turnouts);
			var routes = $('<div>').addClass('button').attr('id', 'routes');
			routes.append($('<div>').text('Routes').addClass('buttonText'));
			routes.on('click', function(event) {routesShow(event);});
			buttons.append(routes);
			if ($isRoster) {	// Roster
				var roster = $jmri.getRoster($rosterGroup);
				if (roster.length) {
					roster.forEach(function(loco) {
						var rosterCell = $('<div>');
						rosterCell.on('click', function(event) {throttleShow(event, loco.name);});
						rosterCell.addClass('rosterCell');
						bodyFrameInner.append(rosterCell);
						rosterCell.append($('<div>').text(loco.name + ' (' + loco.dccAddress + ')').addClass('locoName'));
						rosterCell.append($('<div>').text(loco.roadName + ((loco.roadNumber !== '') ? ' (' + loco.roadNumber + ')' : '')).addClass('locoRoad'));
						rosterCell.append($('<div>').text(loco.mfg + ((loco.model !== '') ? ' (' + loco.model + ')' : '') + ((loco.owner !== '') ? ' [' + loco.owner + ']' : '')).addClass('locoMfg'));
						var icon = loco.iconFilePath.length > 0;
						var image = loco.imageFilePath.length > 0;
						if (icon || image) {
							rosterCell.append($('<div>').addClass('imageContainer'));
							var img = $('<img>').addClass('imageInitial');
							rosterCell.children('.imageContainer').append(img); 
							img[0].onload = function() {
								var o = $(this);
								o.attr('originalHeight', o.height());
								o.attr('originalWidth', o.width());
								resizeImage(o);
							};
							img.attr('src', '/roster/' + encodeURIComponent(loco.name) + '/' + (icon ? 'icon' : 'image') + '?maxHeight=' + $cellHeightRef);
						}
					});
				}
			} else {	// Panels
				var panels = $jmri.getObjectList('panels');
				panels.forEach(function(itemdata) {
					var item = itemdata.data;
					var panelCell = $('<div>');
					panelCell.on('click', function(event) {openPanel(event, decodeURIComponent(item.name), item.URL);});	// 'decode' because it arrives encoded
					panelCell.addClass('panelCell');
					bodyFrameInner.append(panelCell);
					panelCell.append($('<div>').text(decodeURIComponent(item.name) + ((item.userName) ? ' - ' + item.userName : '')).addClass('panelName'));	// 'decode' because it arrives encoded
					panelCell.append($('<div>').addClass('imageContainer'));
					var img = $('<img>').addClass('imageInitial');
					panelCell.children('.imageContainer').append(img); 
					img[0].onload = function() {
						var o = $(this);
						o.attr('originalHeight', o.height());
						o.attr('originalWidth', o.width());
						resizeImage(o);
					};
					img.attr('src', '/panel/' + decodeURIComponent(item.name) + '?format=png');
				});
			}
			break;
		case 'loco':
			body.attr('locoReady', 'false');
			$help.push(
				(!$inFrame ? 'Press [|::] / [::|] button to\nposition speed control left/right.\n\n' : '') +
				($hasMovement ? 'Click [tilt] / [normal] button to turn\nON/OFF device movement sensor.' +
				'\nIf [tilt] active, press speed control and\nmove your phone/tablet to change speed.\n\n' : '') +
				'Click on loco name to select another one.' +
				''
			);
			if (!$hasMovement) $help.push(
				'This device or browser doesn\'t\nsupport movement detection.' +
				''
			);
			document.title+= ' (loco: ' + $paramLocoName + ')';
			var loco = $jmri.getRosterItem($paramLocoName);
			if (loco) {
				var header = $('#header');
				var emergencyStop = $('<div>').addClass('button').attr('id', 'emergencyStop');
				emergencyStop.append($('<div>').text('STOP').addClass('buttonText'));
				emergencyStop.on('click', function(event) {immediateStop(event);});
				header.append(emergencyStop);
				if ($hasMovement) {
					var movementActive = $('<div>').addClass('button').attr('id', 'movementActive');
					movementActive.append($('<div>').text('xxx').addClass('buttonText'));
					movementActive.on('click', function(event) {manageMovementActive(event);});
					header.append(movementActive);
				}
				var speed = $('<div>');
				speed.addClass('speed');
				speed.attr('speed', 0);
				bodyFrameInner.append(speed);
				speed.append($('<div>').attr('id', 'speedGrid'));
				var speedGrid = $('#speedGrid');
				speedGrid.append($('<div>').attr('id', 'speedLimits'));
				speedGrid.append($('<div>').attr('id', 'speedBar'));
				var speedTouch = $('<div>');
				speedGrid.append(speedTouch.attr('id', 'speedTouch'));
				if ($hasMovement) changeMovementActive($movementActive);
				else {
					if ($hasTouch) {
						speedTouch.on('touchstart', function(event) {speedPressedTouch(event.originalEvent);});
						speedTouch.on('touchmove', function(event) {speedMovingTouch(event.originalEvent);});
						speedTouch.on('touchend', function(event) {speedReleasedTouch(event.originalEvent);});
					} else {
						speedTouch.on('mousedown', function(event) {speedPressedMouse(event);});
						speedTouch.on('mousemove', function(event) {speedMovingMouse(event);});
						speedTouch.on('mouseup', function(event) {speedReleasedMouse(event);});
						speedTouch.on('mouseout', function(event) {speedCanceledMouse(event);});
					}
				}
				var reverse = $('<div>').addClass('button').addClass('direction').attr('id', 'reverse');
				reverse.append($('<div>').text('>').addClass('buttonText'));	// Will rotare 90º right
				reverse.on('click', function(event) {setDirection(event, false);});
				speed.append(reverse);
				var forward = $('<div>').addClass('button').addClass('direction').attr('id', 'forward');
				forward.append($('<div>').text('<').addClass('buttonText'));	// Will rotare 90º right
				forward.on('click', function(event) {setDirection(event, true);});
				speed.append(forward);
				var functionsOuter = $('<div>');
				functionsOuter.addClass('functionsOuter');
				bodyFrameInner.append(functionsOuter);
				var functionsInner = $('<div>');
				functionsInner.addClass('functionsInner');
				functionsOuter.append(functionsInner);
				functionsInner.append($('<div>').text(loco.name + ' (' + loco.dccAddress + ')').addClass('locoName').attr('id', 'locoName').on('click', function(event) {changeLoco(event);}));
				var noFs = true;
				for (var i = 0; i < 29; i++) if (loco.f[i]) noFs = false;
				for (var i = 0; i < 29; i++) {
					if (loco.f[i] || noFs) {
						var func = $('<div>');
						if (noFs || loco.f[i].lockable) {
							func.on('click', function(event) {functionClick(event);});
						} else {
							if ($hasTouch) {
								func.on('touchstart', function(event) {functionPressedTouch(event.originalEvent, $(this));});
								func.on('touchend', function(event) {functionReleasedTouch(event.originalEvent, $(this));});
							} else {
								func.on('mousedown', function(event) {functionPressedMouse(event);});
								func.on('mouseup', function(event) {functionReleasedMouse(event);});
								func.on('mouseout', function(event) {functionCanceledMouse(event);});
							}
						}
						func.addClass('functionButton').attr('id', 'locoFunction' + i);
						func.attr('state', $jmri.NO);
						functionsInner.append(func);
						var funcLabel = $('<div>');
						funcLabel.text((loco.f[i] && loco.f[i].functionlabel.length) ? loco.f[i].functionlabel : 'F' + i);
						funcLabel.addClass('funcLabel');
						func.append(funcLabel);
						var funcState = $('<div>');
						funcState.addClass('funcState');
						func.append(funcState);
					}
				}
				var icon = loco.iconFilePath.length > 0;
				var image = loco.imageFilePath.length > 0;
				if (icon || image) {
					var locoCell = $('<div>');
					locoCell.addClass('locoCell');
					functionsInner.append(locoCell);
					var img = $('<img>').addClass('imageInitial');
					locoCell.append(img); 
					img[0].onload = function() {
						var o = $(this);
						o.attr('originalHeight', o.height());
						o.attr('originalWidth', o.width());
						resizeImage(o);
					};
					img.attr('src', '/roster/' + encodeURIComponent(loco.name) + '/' + (icon ? 'icon' : 'image') + '?maxHeight=' + $cellHeightRef);
				}
				$locoAddress = '' + loco.dccAddress;
                $jmri.setJMRI('throttle', $locoAddress, {"rosterEntry":loco.name});
			} else smoothAlert('Loco \'' + $paramLocoName + '\' doesn\'t exist.\nReopen the web page with a valid loco name.');
			break;
		case 'turnouts':
			$help.push(
				'C - closed' +
				'\nT - thrown' +
				'\n? - undefined' +
				''
			);
			document.title+= ' (turnouts)';
			defineTurnoutsRoutes($jmri.getObjectList('turnouts'));
			break;
		case 'routes':
			$help.push(
				'On - on' +
				'\nOff - off' +
				'\nX - disabled' +
				'\n? - undefined' +
				''
			);
			document.title+= ' (routes)';
			defineTurnoutsRoutes($jmri.getObjectList('routes'));
			break;
		case 'panel':
			$help.push(
				'This shows an' +
				'\ninteractive panel.' +
				''
			);
			document.title+= ' (panel: ' + $paramPanelName + ')';
			var iframeAux = $('<div>').attr('id', 'iframeAux');
			var panel = $('<iframe>').attr('src', '/panel/' + $paramPanelName).addClass('panel');
			panel.load(function() {
				var bodyFrameOuter = $('#bodyFrameOuter');
				var bodyFrameInner = $('#bodyFrameInner');
				var panel = $('.panel');
				var panelBody = panel.contents().find('body');
				bodyFrameInner.css('top', 0).css('left', 0);
				panel.css('top', 0).css('left', 0);
				panelBody.css('padding-top', 0).css('padding-bottom', 0);
				panelBody.children('footer').remove();
				panelBody.children('#wrap').children('#panel-area').appendTo(panelBody);
				panelBody.children('#wrap').remove();
				panelBody.children('#panel-area').css('border', 'none').css('position', 'absolute');
				panelBody.css('background-color', $('#iframeAux').css('background-color'))
				setTimeout(function() {$panelLoaded = true; $viewportHeight = 0;}, $resizeCheckInterval * 5);	// Force resize some miliseconds after loading
			});
			iframeAux.css('position', 'absolute').css('background-color', $('body').css('background-color')).css('z-index', '+199');
			iframeAux.css('top', 0).css('left', 0);
			bodyFrameInner.append(iframeAux);
			panel.css('z-index', '+200');
			bodyFrameInner.append(panel);
			break;
	}
	checkLayoutSizeChange();
	$resizeCheckTimer = setInterval(function() {checkLayoutSizeChange();}, $resizeCheckInterval);
	$jmri.getJMRI('power', null);
};

//----------------------------------------- Definition of turnouts or routes
var defineTurnoutsRoutes = function(listTurnoutsRoutes) {
	var bodyFrameInner = $('#bodyFrameInner');
	listTurnoutsRoutes.forEach(function(item) {
		var trCell = $('<div>');
		trCell.addClass('trCell');
		bodyFrameInner.append(trCell);
		trCell.append($('<div>').text(item.data.name + ((item.data.userName) ? ' - ' + item.data.userName : '')).addClass('trName'));
		trCell.append($('<div>').text('').addClass('trStatus').attr('id',encodeId(item.data.name)).attr('state', -1).on('click', function(event) {trChangeStatus(event, item.type, item.data.name);}));
		$jmri.getJMRI( item.type.startsWith ('turnout') ? 'turnout' : 'route', item.data.name);
	});
};

//===================================================================================== Resize functions ==============================

//----------------------------------------- Check screen change size and position (control also 'smoothAlert' messages timeout)
var checkLayoutSizeChange = function() {
	checkSmoothAlertEnd();
	var h = $(window).height();
	var w = $(window).width();
	var speedPosition = loadLocalInfo('webThrottle.speedPosition');
	if (speedPosition == 'true' || speedPosition == 'false') if ($speedPosition != (speedPosition == 'true')) changeSpeedPosition(!$speedPosition);
	var movementActive = loadLocalInfo('webThrottle.movementActive');
	if (movementActive == 'true' || movementActive == 'false') if ($movementActive != (movementActive == 'true')) changeMovementActive(!$movementActive);
	var fS = loadLocalInfo('webThrottle.fontSize');
	if ($fontSize != Number(fS)) {
		$fontChanged = true;
		$fontSize = Number(fS);
	} else $fontChanged = false;
	if ($viewportHeight != h) {
		$heightChanged = true;
		$viewportHeight = h;
	} else $heightChanged = false;
	if ($viewportWidth != w) {
		$widthChanged = true;
		$viewportWidth = w;
	} else $widthChanged = false;
	if ($heightChanged || $widthChanged || $fontChanged) {
		$portrait = (w <= h);
		$sizeCtrlPercent = $fontSize / $defaultFontSize;
		$('body').css('font-size', $fontSize);
		resizeHeader();
		switch ($throttleType) {
			case 'roster':
				resizeRosterLayout();
				break;
			case 'loco':
				resizeThrottleLayout();
				break;
			case 'turnouts':
				resizeTurnoutsRoutesLayout();
				break;
			case 'routes':
				resizeTurnoutsRoutesLayout();
				break;
			case 'panel':
				resizePanelLayout();
				break;
		}
		resizeSelectionList();
		resizeSmoothAlerts();
	}
};

//----------------------------------------- [after image loaded and from 'checkLayoutSizeChange()'] Image resize (related to parent)
var resizeImage = function(img) {
	if (!img.length) return;
	img.css('margin', 'auto');
	img.height(img.attr('originalHeight'));
	img.width(img.attr('originalWidth'));
	if (img.parent().height() / img.parent().width() <= img.outerHeight(false) / img.outerWidth(false))  {
		setOuterHeight(img, img.parent().height(), false);
		img.width('auto');
	} else {
		setOuterWidth(img, img.parent().width(), false);
		img.height('auto');
		img.css('margin-top', (img.parent().height() - img.outerHeight(false)) / 2);
	}
	if (img.hasClass('imageInitial')) img.removeClass('imageInitial').addClass('image');
};

//----------------------------------------- [from 'checkLayoutSizeChange()'] Header layout resize when screen or font changes size
var resizeHeader = function() {
	$nextBlockTop = 0;
	var headerWidth = $(window).width();
	var headerHeight = $sizeCtrlPercent * $headerHeightRef;
	var header = $('#header');
//header.on('mousedown mouseup click mouseout mousemove', function(event) {mouse(event);});
//header.on('touchstart touchend touchcancel touchmove', function(event) {touch(event, $(this));});
	header.css('top', $nextBlockTop).css('left', 0);
	setOuterHeight(header, headerHeight, true);
	setOuterWidth(header, headerWidth - (($showScrollBar && $throttleType != 'loco') ? $vScrollbarWidth : 0), true);
	var power = $('#power');
	if (power.length) {
		setOuterHeight(power, header.height(), true);
		setOuterWidth(power, header.height(), true);
		setTopFromParentContent(power, 0);
		setLeftFromParentContent(power, 0);
		var powerText = power.children('.buttonText');
		powerText.css('top', (power.outerHeight(true) - powerText.outerHeight(true)) / 2 - powerText.outerHeight(true) * $fontDelta);
		setLeftFromParentContent(powerText, 0);
		setOuterWidth(powerText, power.width(), true);
	}
	if ($throttleType == 'roster' || $inFrame) {	// Button to define new throttles in frame or not OR Button to remove itself from frame
		var btFrame = (!$inFrame ? $('#toFrame') : $('#removeFrame'));
		setOuterHeight(btFrame, header.height(), true);
		setOuterWidth(btFrame, header.height() * (($throttleType == 'roster') ? 2 : 1), true);
		setTopFromParentContent(btFrame, 0);
		setLeftFromParentContent(btFrame, power.length ? power.outerWidth(true) + header.height() * 0.2 : 0);
		var btFrameText = btFrame.children('.buttonText');
		btFrameText.css('top', (btFrame.outerHeight(true) - btFrameText.outerHeight(true)) / 2 - btFrameText.outerHeight(true) * $fontDelta);
		setLeftFromParentContent(btFrameText, 0);
		setOuterWidth(btFrameText, btFrame.width(), true);
	}
	if ($throttleType == 'loco' && !$pageInFrame) {	// Button to select speed control position
		var speedPosition = $('#speedPosition');
		setOuterHeight(speedPosition, header.height(), true);
		setOuterWidth(speedPosition, header.height(), true);
		setTopFromParentContent(speedPosition, 0);
		setLeftFromParentContent(speedPosition, power.outerWidth(true) + header.height() * 0.2);
		var speedPositionText = speedPosition.children('.buttonText');
		speedPositionText.css('top', (speedPosition.outerHeight(true) - speedPositionText.outerHeight(true)) / 2 - speedPositionText.outerHeight(true) * $fontDelta);
		setLeftFromParentContent(speedPositionText, 0);
		setOuterWidth(speedPositionText, speedPosition.width(), true);
	}
	var help = $('#help');
	setOuterHeight(help, header.height(), true);
	setOuterWidth(help, header.height() * 0.6, true);
	setTopFromParentContent(help, 0);
	setLeftFromParentContent(help, header.width() - help.outerWidth(true));
	var helpText = help.children('.buttonText');
	helpText.css('top', (help.outerHeight(true) - helpText.outerHeight(true)) / 2 - helpText.outerHeight(true) * $fontDelta);
	setLeftFromParentContent(helpText, 0);
	setOuterWidth(helpText, help.width(), true);
	var fontLarger = $('#fontLarger');
	if (fontLarger.length) {
		setOuterHeight(fontLarger, header.height(), true);
		setOuterWidth(fontLarger, header.height() * 0.6, true);
		setTopFromParentContent(fontLarger, 0);
		setLeftFromParentContent(fontLarger, header.width() - fontLarger.outerWidth(true) - help.outerWidth(true));
		var fontLargerText = fontLarger.children('.buttonText');
		fontLargerText.css('top', (fontLarger.outerHeight(true) - fontLargerText.outerHeight(true)) / 2 - fontLargerText.outerHeight(true) * $fontDelta);
		setLeftFromParentContent(fontLargerText, 0);
		setOuterWidth(fontLargerText, fontLarger.width(), true);
	}
	var fontSmaller = $('#fontSmaller');
	if (fontSmaller.length) {
		setOuterHeight(fontSmaller, header.height(), true);
		setOuterWidth(fontSmaller, header.height() * 0.6, true);
		setTopFromParentContent(fontSmaller, 0);
		setLeftFromParentContent(fontSmaller, header.width() - fontSmaller.outerWidth(true) - (fontLarger.length ? fontLarger.outerWidth(true) : 0) - help.outerWidth(true));
		var fontSmallerText = fontSmaller.children('.buttonText');
		fontSmallerText.css('top', (fontSmaller.outerHeight(true) - fontSmallerText.outerHeight(true)) / 2 - fontSmallerText.outerHeight(true) * $fontDelta);
		setLeftFromParentContent(fontSmallerText, 0);
		setOuterWidth(fontSmallerText, fontSmaller.width(), true);
	}
	$nextBlockTop+= header.outerHeight(true);
};

//----------------------------------------- [from 'checkLayoutSizeChange()'] Roster layout resize when screen or font changes size
var resizeRosterLayout = function() {
	var h = $(window).height();
	var w = $(window).width();
	var bodyFrameOuter = $('#bodyFrameOuter');
	var bodyFrameInner = $('#bodyFrameInner');
	var cellWidthCtrl;
	var horizontalCells;
	var cellWidth;
	var cellHeight;
	bodyFrameOuter.css('top', 0).css('left', 0);
	setOuterHeight(bodyFrameOuter, h, true);
	setOuterWidth(bodyFrameOuter, w, true);
	bodyFrameInner.css('top', 0).css('left', 0);
	setOuterWidth(bodyFrameInner, bodyFrameOuter.width() - ($showScrollBar ? $vScrollbarWidth : 0), true);
	var buttonsWidth = bodyFrameInner.width();
	var buttonsHeight = $sizeCtrlPercent * $buttonsHeightRef;
	var buttons = $('#buttons');
	buttons.css('top', $nextBlockTop).css('left', 0);
	setOuterHeight(buttons, buttonsHeight, true);
	setOuterWidth(buttons, buttonsWidth, true);
	var groups = $('#groups');
	setOuterHeight(groups, buttons.height(), true);
	setOuterWidth(groups, buttons.width() / 2, true);
	setTopFromParentContent(groups, 0);
	setLeftFromParentContent(groups, 0);
	var groupsText = groups.children('.buttonText');
	groupsText.css('top', (groups.outerHeight(true) - groupsText.outerHeight(true)) / 2 - groupsText.outerHeight(true) * $fontDelta);
	setLeftFromParentContent(groupsText, 0);
	setOuterWidth(groupsText, groups.width(), true);
	var turnouts = $('#turnouts');
	setOuterHeight(turnouts, buttons.height(), true);
	setOuterWidth(turnouts, buttons.width() / 4, true);
	setTopFromParentContent(turnouts, 0);
	setLeftFromParentContent(turnouts, groups.outerWidth(true));
	var turnoutsText = turnouts.children('.buttonText');
	turnoutsText.css('top', (turnouts.outerHeight(true) - turnoutsText.outerHeight(true)) / 2 - turnoutsText.outerHeight(true) * $fontDelta);
	setLeftFromParentContent(turnoutsText, 0);
	setOuterWidth(turnoutsText, turnouts.width(), true);
	var routes = $('#routes');
	setOuterHeight(routes, buttons.height(), true);
	setOuterWidth(routes, buttons.width() / 4, true);
	setTopFromParentContent(routes, 0);
	setLeftFromParentContent(routes, buttons.width() - routes.outerWidth(true));
	var routesText = routes.children('.buttonText');
	routesText.css('top', (routes.outerHeight(true) - routesText.outerHeight(true)) / 2 - routesText.outerHeight(true) * $fontDelta);
	setLeftFromParentContent(routesText, 0);
	setOuterWidth(routesText, routes.width(), true);
	var t = $nextBlockTop + buttons.outerHeight(true);
	var l = 0;
	if ($isRoster) {	// Roster
		var rosterCell = $('.rosterCell');
		cellWidthCtrl = $sizeCtrlPercent * $cellWidthRef;
		horizontalCells = Math.floor(bodyFrameInner.width() / cellWidthCtrl);
		cellWidth = (horizontalCells == 0) ? bodyFrameInner.width() : bodyFrameInner.width() / horizontalCells;
		var cellHeightIni = $sizeCtrlPercent * $cellHeightRef;
		rosterCell.each(function(index) {
			var o = $(this);
			var locoImageContainer = o.children('.imageContainer');
			if (horizontalCells == 0 && locoImageContainer.length) cellHeight = cellHeightIni * 2; else cellHeight = cellHeightIni;
			o.css('top', t).css('left', l);
			setOuterHeight(o, cellHeight, true);
			setOuterWidth(o, cellWidth, true);
			if (horizontalCells == 0) {
				t+= cellHeight;
				l = 0;
			} else {
				if ((l + cellWidth * 2) > bodyFrameInner.width()) {
					t+= cellHeight;
					l = 0;
				} else l+= cellWidth;
			}
			var locoName = o.children('.locoName');
			var locoRoad = o.children('.locoRoad');
			var locoMfg = o.children('.locoMfg');
			if (locoImageContainer.length) {
				setTopFromParentContent(locoImageContainer, 0);
				setLeftFromParentContent(locoImageContainer, 0);
				if (horizontalCells == 0) {
					setOuterHeight(locoImageContainer, o.height() / 2, true);
					setOuterWidth(locoImageContainer, o.width(), true);
					locoName.css('top', (cellHeight / 2 + (cellHeight / 2 - locoName.outerHeight(true)) / 2 - locoName.outerHeight(true) * $fontDelta) - o.height() / 2 * 0.25);
					locoRoad.css('top', (cellHeight / 2 + (cellHeight / 2 - locoRoad.outerHeight(true)) / 2 - locoRoad.outerHeight(true) * $fontDelta));
					locoMfg.css('top', (cellHeight / 2 + (cellHeight / 2 - locoMfg.outerHeight(true)) / 2 - locoMfg.outerHeight(true) * $fontDelta) + o.height() / 2 * 0.25);
					setLeftFromParentContent(locoName, 0);
					setLeftFromParentContent(locoRoad, 0);
					setLeftFromParentContent(locoMfg, 0);
					setOuterWidth(locoName, o.width(), true);
					setOuterWidth(locoRoad, o.width(), true);
					setOuterWidth(locoMfg, o.width(), true);
				} else {
					setOuterHeight(locoImageContainer, o.height(), true);
					setOuterWidth(locoImageContainer, o.width() / 2, true);
					locoName.css('top', ((cellHeight - locoName.outerHeight(true)) / 2 - locoName.outerHeight(true) * $fontDelta) - o.height() * 0.30);
					locoRoad.css('top', ((cellHeight - locoRoad.outerHeight(true)) / 2 - locoRoad.outerHeight(true) * $fontDelta));
					locoMfg.css('top', ((cellHeight - locoMfg.outerHeight(true)) / 2 - locoMfg.outerHeight(true) * $fontDelta) + o.height() * 0.30);
					setLeftFromParentContent(locoName, locoImageContainer.outerWidth(true));
					setLeftFromParentContent(locoRoad, locoImageContainer.outerWidth(true));
					setLeftFromParentContent(locoMfg, locoImageContainer.outerWidth(true));
					setOuterWidth(locoName, o.width() / 2, true);
					setOuterWidth(locoRoad, o.width() / 2, true);
					setOuterWidth(locoMfg, o.width() / 2, true);
				} 
				resizeImage(locoImageContainer.children('.image'));
			} else {
				locoName.css('top', ((cellHeight - locoName.outerHeight(true)) / 2 - locoName.outerHeight(true) * $fontDelta) - o.height() * 0.25);
				locoRoad.css('top', ((cellHeight - locoRoad.outerHeight(true)) / 2 - locoRoad.outerHeight(true) * $fontDelta));
				locoMfg.css('top', ((cellHeight - locoMfg.outerHeight(true)) / 2 - locoMfg.outerHeight(true) * $fontDelta) + o.height() * 0.25);
				setLeftFromParentContent(locoName, 0);
				setLeftFromParentContent(locoRoad, 0);
				setLeftFromParentContent(locoMfg, 0);
				setOuterWidth(locoName, o.width(), true);
				setOuterWidth(locoRoad, o.width(), true);
				setOuterWidth(locoMfg, o.width(), true);
			}
		});
	} else {	// Panels
		var panelCell = $('.panelCell');
		cellWidthCtrl = $sizeCtrlPercent * $cellWidthRef * 2;
		horizontalCells = Math.floor(bodyFrameInner.width() / cellWidthCtrl) + 1;
		cellWidth = bodyFrameInner.width() / horizontalCells;
		cellHeight = $sizeCtrlPercent * $cellHeightRef * 0.5;
		cellHeight*= 4;
		panelCell.each(function(index) {
			var o = $(this);
			o.css('top', t).css('left', l);
			setOuterHeight(o, cellHeight, true);
			setOuterWidth(o, cellWidth, true);
			if ((l + cellWidth * 2) > bodyFrameInner.width()) {
				t+= cellHeight;
				l = 0;
			} else l+= cellWidth;
			var panelImageContainer = o.children('.imageContainer');
			var panelName = o.children('.panelName');
			setTopFromParentContent(panelImageContainer, 0);
			setLeftFromParentContent(panelImageContainer, 0);
			setOuterHeight(panelImageContainer, o.height() / 4 * 3, true);
			setOuterWidth(panelImageContainer, o.width(), true);
			panelName.css('top', cellHeight / 4 * 3.2);
			setLeftFromParentContent(panelName, 0);
			setOuterWidth(panelName, o.width(), true);
			resizeImage(panelImageContainer.children('.image'));
		});
	}
	if (l != 0) t+= cellHeight;
	setOuterHeight(bodyFrameInner, t, true);
	if (bodyFrameOuter.height() < bodyFrameInner.outerHeight(true) && !$showScrollBar) {
		$showScrollBar = true;
		resizeHeader();
		resizeRosterLayout();
	}
	if (bodyFrameOuter.height() >= bodyFrameInner.outerHeight(true) && $showScrollBar) {
		$showScrollBar = false;
		resizeHeader();
		resizeRosterLayout();
	}
};

//----------------------------------------- [from 'checkLayoutSizeChange()'] Throttle layout resize when screen or font changes size
var resizeThrottleLayout = function() {
	var h = $(window).height();
	var w = $(window).width();
	var bodyFrameOuter = $('#bodyFrameOuter');
	var bodyFrameInner = $('#bodyFrameInner');
	bodyFrameOuter.css('top', 0).css('left', 0);
	setOuterHeight(bodyFrameOuter, h, true);
	setOuterWidth(bodyFrameOuter, w, true);
	bodyFrameInner.css('top', 0).css('left', 0);
	setOuterHeight(bodyFrameInner, bodyFrameOuter.height(), true);
	setOuterWidth(bodyFrameInner, bodyFrameOuter.width(), true);
	var emergencyStop = $('#emergencyStop');
	if (!emergencyStop.length) return;
	if ($pageInFrame) $speedPosition = window.parent.$('#' + encodeId(document.URL)).attr('rightSide');
	if ($speedPosition == 'true' || $speedPosition == 'false') $speedPosition = ($speedPosition == 'true');
	var headerHeight = $sizeCtrlPercent * $headerHeightRef;
	var header = $('#header');
	var fontSmaller = $('#fontSmaller');
	var fontLarger = $('#fontLarger');
	var help = $('#help');
	setOuterHeight(emergencyStop, header.height(), true);
	setOuterWidth(emergencyStop, header.height() * 2, true);
	setTopFromParentContent(emergencyStop, 0);
	setLeftFromParentContent(emergencyStop, header.width() - emergencyStop.outerWidth(true) - (fontSmaller.length ? fontSmaller.outerWidth(true) : 0) - (fontLarger.length ? fontLarger.outerWidth(true) : 0) - help.outerWidth(true));
	var emergencyStopText = emergencyStop.children('.buttonText');
	emergencyStopText.css('top', (emergencyStop.outerHeight(true) - emergencyStopText.outerHeight(true)) / 2 - emergencyStopText.outerHeight(true) * $fontDelta);
	setLeftFromParentContent(emergencyStopText, 0);
	setOuterWidth(emergencyStopText, emergencyStop.width(), true);
	var movementActive = $('#movementActive');
	if (movementActive.length) {
		setOuterHeight(movementActive, header.height(), true);
		setOuterWidth(movementActive, header.height() * 2, true);
		setTopFromParentContent(movementActive, 0);
		setLeftFromParentContent(movementActive, $pageInFrame ? $('#removeFrame').outerWidth(true) + header.height() * 0.2 : $('#power').outerWidth(true) + header.height() * 0.2 + $('#speedPosition').outerWidth(true) + header.height() * 0.2);
		var movementActiveText = movementActive.children('.buttonText');
		movementActiveText.css('top', (movementActive.outerHeight(true) - movementActiveText.outerHeight(true)) / 2 - movementActiveText.outerHeight(true) * $fontDelta);
		setLeftFromParentContent(movementActiveText, 0);
		setOuterWidth(movementActiveText, movementActive.width(), true);
	}
	var speed = $('.speed');
	var speedWidth = $sizeCtrlPercent * $speedWidthRef;
	if (speedWidth < bodyFrameInner.width() / 8) speedWidth = bodyFrameInner.width() / 8;
	speed.css('top', $nextBlockTop).css('left', $speedPosition ? bodyFrameInner.width() - speedWidth : 0);
	setOuterHeight(speed, bodyFrameInner.height() - $nextBlockTop, true);
	setOuterWidth(speed, speedWidth, true);
	var buttonsSize = speed.width() / 2;
	var speedGrid = $('#speedGrid');
	setOuterHeight(speedGrid, speed.height() - buttonsSize, true);
	setOuterWidth(speedGrid, speedWidth, true);
	setTopFromParentContent(speedGrid, 0);
	setLeftFromParentContent(speedGrid, 0);
	var speedLimits = $('#speedLimits');
	setOuterHeight(speedLimits, speedGrid.height() - buttonsSize / 2, true);
	setOuterWidth(speedLimits, speedGrid.width(), true);
	setTopFromParentContent(speedLimits, 0);
	setLeftFromParentContent(speedLimits, 0);
	showSpeed();
	var speedTouch = $('#speedTouch');
	setOuterHeight(speedTouch, speedGrid.height(), true);
	setOuterWidth(speedTouch, speedGrid.width(), true);
	setTopFromParentContent(speedTouch, 0);
	setLeftFromParentContent(speedTouch, 0);
	var reverse = $('#reverse');
	setOuterHeight(reverse, buttonsSize, true);
	setOuterWidth(reverse, buttonsSize, true);
	setTopFromParentContent(reverse, speed.height() - reverse.outerHeight(true));
	setLeftFromParentContent(reverse, 0);
	var reverseText = reverse.children('.buttonText');
	reverseText.css('top', (reverse.outerHeight(true) - reverseText.outerHeight(true)) / 2 - reverseText.outerHeight(true) * $fontDelta);
	setLeftFromParentContent(reverseText, 0);
	setOuterWidth(reverseText, reverse.width(), true);
	var forward = $('#forward');
	setOuterHeight(forward, reverse.outerHeight(true), true);
	setOuterWidth(forward, reverse.outerWidth(true), true);
	setTopFromParentContent(forward, speed.height() - forward.outerHeight(true));
	setLeftFromParentContent(forward, speed.width() - forward.outerWidth(true));
	var forwardText = forward.children('.buttonText');
	forwardText.css('top', (forward.outerHeight(true) - forwardText.outerHeight(true)) / 2 - forwardText.outerHeight(true) * $fontDelta);
	setLeftFromParentContent(forwardText, 0);
	setOuterWidth(forwardText, forward.width(), true);
	var functionsOuter = $('.functionsOuter');
	var functionsInner = $('.functionsInner');
	functionsOuter.css('top', $nextBlockTop).css('left', $speedPosition ? 0 : speedWidth);
	setOuterHeight(functionsOuter, bodyFrameInner.height() - $nextBlockTop, true);
	setOuterWidth(functionsOuter, bodyFrameInner.width() - speedWidth, true);
	functionsInner.css('top', 0).css('left', 0);
	setOuterWidth(functionsInner, functionsOuter.width() - ($showScrollBar ? $vScrollbarWidth : 0), true);
	var cellWidthCtrl = $sizeCtrlPercent * $functionWidthRef;
	var horizontalCells = Math.floor(functionsInner.width() / cellWidthCtrl) + 1;
	var cellWidth = functionsInner.width() / horizontalCells;
	var cellHeight = $sizeCtrlPercent * $functionHeightRef;
	var t = 0;
	var l = 0;
	var locoName = $('.locoName');
	locoName.css('top', t).css('left', l);
	setOuterWidth(locoName, functionsInner.width(), true);
	t+= locoName.outerHeight(true);
	for (var i = 0; i < 29; i++) {
		var func = $('#locoFunction' + i);
		if (func.length) {
			func.css('top', t).css('left', l);
			setOuterHeight(func, cellHeight, true);
			setOuterWidth(func, cellWidth, true);
			if ((l + cellWidth * 2) > functionsInner.width()) {
				t+= cellHeight;
				l = 0;
			} else l+= cellWidth;
			var funcLabel = func.children('.funcLabel');
			funcLabel.css('top', ((cellHeight - funcLabel.outerHeight(true)) / 2 - funcLabel.outerHeight(true) * $fontDelta)).css('left', 0);
			setOuterWidth(funcLabel, func.width(), true);
			var funcState = func.children('.funcState');
			setOuterHeight(funcState, func.height() / 10, true);
			funcState.css('top', func.height() - funcState.outerHeight(true)).css('left', 0);
			setOuterWidth(funcState, func.width(), true);
		}
	}
	if (l != 0) t+= cellHeight;
	var locoHeight = $sizeCtrlPercent * $cellHeightRef;
	var locoCell = $('.locoCell');
	if (locoCell.length) {
		locoCell.css('top', t).css('left', 0);
		setOuterHeight(locoCell, locoHeight, true);
		setOuterWidth(locoCell, functionsInner.width(), true);
		t+= locoCell.outerHeight(true);
		resizeImage(locoCell.children('.image'));
	}
	setOuterHeight(functionsInner, t, true);
	if (functionsOuter.height() < functionsInner.outerHeight(true) && !$showScrollBar) {
		$showScrollBar = true;
		resizeThrottleLayout();
	}
	if (functionsOuter.height() >= functionsInner.outerHeight(true) && $showScrollBar) {
		$showScrollBar = false;
		resizeThrottleLayout();
	}
};

//----------------------------------------- [from 'checkLayoutSizeChange()'] Turnouts and Routes layout resize when screen or font changes size
var resizeTurnoutsRoutesLayout = function() {
	var h = $(window).height();
	var w = $(window).width();
	var bodyFrameOuter = $('#bodyFrameOuter');
	var bodyFrameInner = $('#bodyFrameInner');
	bodyFrameOuter.css('top', 0).css('left', 0);
	setOuterHeight(bodyFrameOuter, h, true);
	setOuterWidth(bodyFrameOuter, w, true);
	bodyFrameInner.css('top', 0).css('left', 0);
	setOuterWidth(bodyFrameInner, bodyFrameOuter.width() - ($showScrollBar ? $vScrollbarWidth : 0), true);
	var trCell = $('.trCell');
	var cellWidthCtrl = $sizeCtrlPercent * $cellWidthRef * 1.5;
	var horizontalCells = Math.floor(bodyFrameInner.width() / cellWidthCtrl) + 1;
	var cellWidth = bodyFrameInner.width() / horizontalCells;
	var cellHeight = $sizeCtrlPercent * $cellHeightRef * 0.5;
	var t = $nextBlockTop;
	var l = 0;
	trCell.each(function(index) {
		var o = $(this);
		o.css('top', t).css('left', l);
		setOuterHeight(o, cellHeight, true);
		setOuterWidth(o, cellWidth, true);
		if ((l + cellWidth * 2) > bodyFrameInner.width()) {
			t+= cellHeight;
			l = 0;
		} else l+= cellWidth;
		var trName = o.children('.trName');
		var trStatus = o.children('.trStatus');
		trName.css('top', ((cellHeight - trName.outerHeight(true)) / 2 - trName.outerHeight(true) * $fontDelta));
		trStatus.css('top', trName.css('top'));
		setOuterWidth(trName, o.width() * 0.8, true);
		setOuterWidth(trStatus, o.width() - trName.outerWidth(true), true);
		setLeftFromParentContent(trName, 0);
		setLeftFromParentContent(trStatus, trName.outerWidth(true));
	});
	if (l != 0) t+= cellHeight;
	setOuterHeight(bodyFrameInner, t, true);
	if (bodyFrameOuter.height() < bodyFrameInner.outerHeight(true) && !$showScrollBar) {
		$showScrollBar = true;
		resizeHeader();
		resizeTurnoutsRoutesLayout();
	}
	if (bodyFrameOuter.height() >= bodyFrameInner.outerHeight(true) && $showScrollBar) {
		$showScrollBar = false;
		resizeHeader();
		resizeTurnoutsRoutesLayout();
	}
};

//----------------------------------------- [from 'checkLayoutSizeChange()'] Panel layout resize when screen or font changes size
var resizePanelLayout = function() {
	if (!$panelLoaded) return;	// If loading not complete, give up! (onload event will force resize)
	$nextBlockTop-= $('#header').outerHeight(true);
	var h = $(window).height();
	var w = $(window).width();
	var bodyFrameOuter = $('#bodyFrameOuter');
	var bodyFrameInner = $('#bodyFrameInner');
	var iframeAux = $('#iframeAux');
	var panel = $('.panel');
	var panelBody = panel.contents().find('body');
	var panelArea = panelBody.children('#panel-area');
	var offsetV = 0;
	var scrollbarV = 0;
	var offsetH = 0;
	var scrollbarH = 0;
	setOuterHeight(bodyFrameOuter, h, true);
	setOuterWidth(bodyFrameOuter, w, true);
	setOuterHeight(bodyFrameInner, bodyFrameOuter.height(), true);
	setOuterWidth(bodyFrameInner, bodyFrameOuter.width(), true);
	setOuterHeight(iframeAux, bodyFrameInner.height(), true);
	setOuterWidth(iframeAux, bodyFrameInner.width(), true);
	setOuterHeight(panel, bodyFrameInner.height(), true);
	setOuterWidth(panel, bodyFrameInner.width(), true);
	if (panel.height() >= panelArea.height()) offsetV = (panel.height() - panelArea.height()) / 2;
	else scrollbarV = $vScrollbarWidth;
	if (panel.width() >= panelArea.width()) offsetH = (panel.width() - panelArea.width()) / 2;
	else scrollbarH = $vScrollbarWidth;
	if (scrollbarV > 0 && scrollbarH > 0) {
		scrollbarV = 0;
		scrollbarH = 0;
	}
	panelArea.css('top', offsetV + scrollbarH);
	panelArea.css('left', offsetH + scrollbarV);
};

//----------------------------------------- [from 'checkLayoutSizeChange()' and 'selectionList'] Selection list layout resize when screen or font changes size
var resizeSelectionList = function() {
	var o = $('.selectionList');
	if (!o.length) return;
	var h = $(window).height();
	var w = o.parent().width();
	o.css('top', (h - o.outerHeight(true)) / 2);
	o.css('left', (w - o.outerWidth(true)) / 2);
};

//----------------------------------------- [from 'checkLayoutSizeChange()' and 'smoothAlert'] Smooth Alerts layout resize when screen or font changes size
var resizeSmoothAlerts = function() {
	var os = $('.smoothAlert');
	var h = $(window).height();
	var w = os.eq(0).parent().width();
	var auxPosition = (os.length - 1) / 2;
	os.each(function(index) {
		var o = $(this);
		o.css('top', (h - o.outerHeight(true)) / 2 - 1.5 * (index - auxPosition) * Number(o.css('border-top-width').split('px')[0]));
		o.css('left', (w - o.outerWidth(true)) / 2 + 1.5 * (index - auxPosition) * Number(o.css('border-left-width').split('px')[0]));
	});
};

//===================================================================================== Actions and Auxiliary functions ===============

//----------------------------------------- Check smooth alert end
var checkSmoothAlertEnd = function() {
	$('.smoothAlert').each(function(index) {
		var o = $(this);
		var sec = o.attr('seconds');
		if (!isNaN(sec)) {
			if (sec <= -1) o.remove();
			else {
				if (sec < 1.5 && !o.hasClass('fadeOut')) o.addClass('fadeOut');
				o.attr('seconds', sec - $resizeCheckInterval / 1000);
			}
		}
	});
};

//----------------------------------------- Change power state
var changePowerState = function() {
	var lastState = $('#power').attr('state');
	switch (Number(lastState)) {
		case $jmri.powerOFF:
			$jmri.setJMRI('power', null, {"state":$jmri.powerON});
			break;
		case $jmri.powerON:
			$jmri.setJMRI('power', null, {"state":$jmri.powerOFF});
			break;
		default:
			$jmri.setJMRI('power', null, {"state":$jmri.powerON});
			break;
	}
};

//----------------------------------------- Remove frame
var removeFrame = function() {
	removeFromFrameList(document.URL);
	window.open(document.URL.split('?')[0], '_top');
	return;
};

//----------------------------------------- Load frames list {$frameList[]}
var loadFrameList = function() {
	var l = loadLocalInfo('webThrottle.frameListSize');
	if (l && !isNaN(l) && Number(l) >= 0 && Number(l) == Math.abs(l)) l = Number(l); else saveLocalInfo('webThrottle.frameListSize', l = 0);
	for (var i = 0; i < l; i++) $frameList[i] = loadLocalInfo('webThrottle.frameList[' + i + ']');
};

//----------------------------------------- Save frames list {$frameList[]}
var saveFrameList = function() {
	saveLocalInfo('webThrottle.frameListSize', $frameList.length);
	for (var i = 0; i < $frameList.length; i++) saveLocalInfo('webThrottle.frameList[' + i + ']', $frameList[i]);
};

//----------------------------------------- Add URL to frames list {$frameList[]}
var addToFrameList = function(url) {
	loadFrameList();
	var find = false;
	for (var i = 0; i < $frameList.length; i++) {
		if ($frameList[i] == url) {
			find = true;
			break;
		}
	}
	if (!find) {
		$frameList.push(url);
		saveFrameList();
	}
	return !find;
};

//----------------------------------------- Replace URL in frames list {$frameList[]}
var replaceInFrameList = function(urlOld, urlNew) {
	loadFrameList();
	for (var i = 0; i < $frameList.length; i++) if ($frameList[i] == urlNew) return false;
	for (var i = 0; i < $frameList.length; i++) {
		if ($frameList[i] == urlOld) {
			$frameList[i] = urlNew;
			break;
		}
	}
	saveFrameList();
	return true;
};

//----------------------------------------- Remove URL from frames list {$frameList[]}
var removeFromFrameList = function(url) {
	loadFrameList();
	var item = -1;
	for (var i = 0; i < $frameList.length; i++) {
		if ($frameList[i] == url) {
			item = i;
			break;
		}
	}
	if (item >= 0) {
		$frameList.splice(item, 1);
		saveFrameList();
	}
};

//----------------------------------------- Show speed bar
var showSpeed = function() {
	var speed = $('.speed');
	var speedLimits = $('#speedLimits');
	var speedBar = $('#speedBar');
	var speedValue = Number(speed.attr('speed'));
	if (speedValue < 0) speedValue = 0;
	var speedOffset = speedLimits.height() * speedValue;
	if (speedValue > 0 && speedOffset < 1) speedOffset = 1;
	speedBar.height(speedOffset);
	speedBar.width(speedLimits.width());
	setTopFromParentContent(speedBar, speedLimits.height() - speedOffset);
	setLeftFromParentContent(speedBar, 0);
};

//===================================================================================== Responses from server =========================

//----------------------------------------- [from server] Layout power state
var layoutPowerState = function(state) {
	var power = $('#power');
	power.attr('state', state);
	power.removeClass('powerUnknown powerOff powerOn');
	switch (state) {
		case $jmri.powerOFF:
			power.addClass('powerOff');
			break;
		case $jmri.powerON:
			power.addClass('powerOn');
			break;
		default:
			power.addClass('powerUnknown');
			break;
	}
};

//----------------------------------------- [from server] Last selected throttle address
var throttleState = function(name, address, speed, forward, fs) {
	if (name != $locoAddress) return;
	$('body').attr('locoReady', 'true');
	if (forward != undefined) throttleDirection(name, forward);
	if (speed != undefined) throttleSpeed(name, speed);
	for (var i = 0; i < 29; i++) if (fs[i] != undefined) throttleFunctionState(name, i, fs[i]);
};

//----------------------------------------- Throttle direction
var throttleDirection = function(name, forward) {
	if (name != $locoAddress) return;
	var _reverse = $('#reverse');
	var _forward = $('#forward');
	_reverse.removeClass('directionActive directionInactive');
	_forward.removeClass('directionActive directionInactive');
	if (forward == $jmri.TRUE) {
		_reverse.addClass('directionInactive');
		_forward.addClass('directionActive');
	} else {
		_reverse.addClass('directionActive');
		_forward.addClass('directionInactive');
	}
};

//----------------------------------------- Throttle speed
var throttleSpeed = function(name, speed) {
	if (name != $locoAddress) return;
	$speedAux = speed;
	if (!$speedFeedback) return;
	$('.speed').attr('speed', speed);
	showSpeed();
};

//----------------------------------------- Throttle function state
var throttleFunctionState = function(name, functionNumber, active) {
	if (name != $locoAddress) return;
	var func = $('#locoFunction' + functionNumber);
	if (func.length) {
		func.attr('state', active);
		var funcState = func.children('.funcState');
		funcState.removeClass('funcOff funcOn');
		if (active == $jmri.TRUE) funcState.addClass('funcOn');
		else funcState.addClass('funcOff');
	}
};

//----------------------------------------- [from server] Layout turnout state
var layoutTurnoutState = function(name, userName, comment, inverted, state) {
	var o = $('#' + encodeId(name));
	o.attr('state', state);
	o.removeClass('turnoutUndefined turnoutThrown turnoutClosed');
	switch (state) {
		case $jmri.turnoutTHROWN:
			o.text('T').addClass('turnoutThrown');
			break;
		case $jmri.turnoutCLOSED:
			o.text('C').addClass('turnoutClosed');
			break;
		default:
			o.text('?').addClass('turnoutUndefined');
			break;
	}
};

//----------------------------------------- [from server] Layout route state
var layoutRouteState = function(name, userName, comment, state) {
	var o = $('#' + encodeId(name));
	o.attr('state', state);
	o.removeClass('routeUndefined routeDisabled routeInactive routeActive');
	switch (state) {
		case $jmri.routeINACTIVE:
			o.text('off').addClass('routeInactive');
			break;
		case $jmri.routeACTIVE:
			o.text('on').addClass('routeActive');
			break;
		case $jmri.routeDISABLED:
			o.text('X').addClass('routeDisabled');
			break;
		default:
			o.text('?').addClass('routeUndefined');
			break;
	}
};

//===================================================================================== Touch and Mouse ===============================

//----------------------------------------- Test event (touch)
var touch = function(_e, o) {
	var e = _e.originalEvent;
	if (e.type != 'touchmove') smoothAlert(
		'*** Touch ***' +
		'\nObject: ' + o.attr('id') + 
		'\nEvent type: ' + e.type + 
		'\nX: ' + e.changedTouches[0].pageX + 
		'\nY: ' + e.changedTouches[0].pageY
	);
	e.stopPropagation();
	e.preventDefault();
};

//----------------------------------------- Test event (mouse)
var mouse = function(e) {
	var o = $(e.currentTarget);
	if (e.type != 'mousemove') smoothAlert(
		'*** Mouse ***' +
		'\nObject: ' + o.attr('id') + 
		'\nEvent type: ' + e.type + 
		'\nButton: ' + ((e.which) ? e.which : e.button) + 
		'\nX: ' + e.pageX + 
		'\nY: ' + e.pageY
	);
	e.stopPropagation();
	e.preventDefault();
};

//----------------------------------------- Left button pressed ? (mouse)
var isLeftButton = function(e) {
	var r = false;
	if (e.which) if (e.which == 1) r = true;
	if (e.button) if (e.button == 0) r = true;
	return r;
};

//----------------------------------------- Power button pressed (touch)
var powerButtonPressedTouch = function(e) {powerButtonPressed(e);};

//----------------------------------------- Power button pressed (mouse)
var powerButtonPressedMouse = function(e) {if (isLeftButton(e)) powerButtonPressed(e);};

//----------------------------------------- Power button pressed
var powerButtonPressed = function(e) {
	e.preventDefault();
	e.stopImmediatePropagation();
	$powerDelayTimer = setTimeout(function() {changePowerState();}, $buttonDelayTimeout);
};

//----------------------------------------- Power button released (touch)
var powerButtonReleasedTouch = function(e) {powerButtonReleased(e);};

//----------------------------------------- Power button released (mouse)
var powerButtonReleasedMouse = function(e) {if (isLeftButton(e)) powerButtonReleased(e);};

//----------------------------------------- Power button released
var powerButtonReleased = function(e) {
	e.preventDefault();
	e.stopImmediatePropagation();
	clearInterval($powerDelayTimer);
};

//----------------------------------------- Power button released or canceled (mouse)
var powerButtonCanceledMouse = function(e) {
	if (!isLeftButton(e)) return;
	e.preventDefault();
	e.stopImmediatePropagation();
	var o = $(e.currentTarget);
	var mouseY = e.pageY;
	var mouseX = e.pageX;
	var oTop = o.offset().top;
	var oLeft = o.offset().left;
	var oBottom = oTop + o.outerHeight(false);
	var oRight = oLeft + o.outerWidth(false);
	if (mouseY > oTop && mouseY < oBottom && mouseX > oLeft && mouseX < oRight) return;
	else clearInterval($powerDelayTimer);
};

//----------------------------------------- Remove button pressed (touch)
var removeButtonPressedTouch = function(e) {removeButtonPressed(e);};

//----------------------------------------- Remove button pressed (mouse)
var removeButtonPressedMouse = function(e) {if (isLeftButton(e)) removeButtonPressed(e);};

//----------------------------------------- Remove button pressed
var removeButtonPressed = function(e) {
	e.preventDefault();
	e.stopImmediatePropagation();
	$removeDelayTimer = setTimeout(function() {removeFrame();}, $buttonDelayTimeout);
};

//----------------------------------------- Remove button released (touch)
var removeButtonReleasedTouch = function(e) {removeButtonReleased(e);};

//----------------------------------------- Remove button released (mouse)
var removeButtonReleasedMouse = function(e) {if (isLeftButton(e)) removeButtonReleased(e);};

//----------------------------------------- Remove button released
var removeButtonReleased = function(e) {
	e.preventDefault();
	e.stopImmediatePropagation();
	clearInterval($removeDelayTimer);
};

//----------------------------------------- Remove button released or canceled (mouse)
var removeButtonCanceledMouse = function(e) {
	if (!isLeftButton(e)) return;
	e.preventDefault();
	e.stopImmediatePropagation();
	var o = $(e.currentTarget);
	var mouseY = e.pageY;
	var mouseX = e.pageX;
	var oTop = o.offset().top;
	var oLeft = o.offset().left;
	var oBottom = oTop + o.outerHeight(false);
	var oRight = oLeft + o.outerWidth(false);
	if (mouseY > oTop && mouseY < oBottom && mouseX > oLeft && mouseX < oRight) return;
	else clearInterval($removeDelayTimer);
};

//----------------------------------------- Change font size - Click (mouse and touch with simulation)
var fontSizeChange = function(e, increment) {
	if (!isLeftButton(e)) return;
	e.preventDefault();
	e.stopImmediatePropagation();
	var fontSize = $fontSize;
	fontSize+= increment;
	if (fontSize < $fontSizeMin) fontSize = $fontSizeMin;
	if (fontSize > $fontSizeMax) fontSize = $fontSizeMax;
	saveLocalInfo('webThrottle.fontSize', fontSize);
	smoothAlert('text size: ' + fontSize + 'px', 2);
};

//----------------------------------------- Show help - Click (mouse and touch with simulation)
var helpShow = function(e) {
	if (!isLeftButton(e)) return;
	e.preventDefault();
	e.stopImmediatePropagation();
	$help.forEach(function(entry) {smoothAlert(entry);});
};

//----------------------------------------- Choose roster group - Click (mouse and touch with simulation)
var chooseGroup = function(e) {
	if (!isLeftButton(e)) return;
	e.preventDefault();
	e.stopImmediatePropagation();
	if (!$rosterGroups.length || $rosterGroups[0] != 'Complete roster') $rosterGroups.unshift('Complete roster');
	if ($rosterGroups[$rosterGroups.length - 1] != '[Panels]') {
		$rosterGroups.push('');
		$rosterGroups.push('[Panels]');
	}
	var f = function(i) {
		if (($rosterGroups[i] != $rosterGroup && i != 0) ||  (i == 0 && $rosterGroup != '')) {
			if (i == 0) saveLocalInfo('webThrottle.rosterGroup', '');
			else saveLocalInfo('webThrottle.rosterGroup', $rosterGroups[i]);
			window.open(document.URL.split('?')[0], '_self');
		}
	};
	var ai = 0;
	for (var i = 0; i < $rosterGroups.length; i++) if ($rosterGroups[i] != '' && $rosterGroups[i] == $rosterGroup) ai = i;
	selectionList($rosterGroups, 'Roster groups', ai, f);
};

//----------------------------------------- Show turnouts - Click (mouse and touch with simulation)
var turnoutsShow = function(e) {
	if (!isLeftButton(e)) return;
	e.preventDefault();
	e.stopImmediatePropagation();
	var url = document.URL.split('?')[0] + '?turnouts';
	if ($toFrame) {
		if (addToFrameList(url + '&inframe')) window.open(document.URL.split('?')[0], $pageInFrame ? '_top' : '_self');
	} else window.open(url, url);
};

//----------------------------------------- Show routes - Click (mouse and touch with simulation)
var routesShow = function(e) {
	if (!isLeftButton(e)) return;
	e.preventDefault();
	e.stopImmediatePropagation();
	var url = document.URL.split('?')[0] + '?routes';
	if ($toFrame) {
		if (addToFrameList(url + '&inframe')) window.open(document.URL.split('?')[0], $pageInFrame ? '_top' : '_self');
	} else window.open(url, url);
};

//----------------------------------------- Show loco throttle - Click (mouse and touch with simulation)
var throttleShow = function(e, locoId) {
	if (!isLeftButton(e)) return;
	e.preventDefault();
	e.stopImmediatePropagation();
	var url = document.URL.split('?')[0] + '?loconame=' + encodeURIComponent(locoId);
	if ($toFrame) {
		if (addToFrameList(url + '&inframe')) window.open(document.URL.split('?')[0], $pageInFrame ? '_top' : '_self');
	} else window.open(url, url);
};

//----------------------------------------- Change turnout or route status - Click (mouse and touch with simulation)
var trChangeStatus = function(e, type, name) {
	if (!isLeftButton(e)) return;
	e.preventDefault();
	e.stopImmediatePropagation();
	var lastState = Number($('#' + encodeId(name)).attr('state'));
	if (type.startsWith ('turnout')) {	// 0(undefined) 1(unknown) 2(Closed) 4(Thrown)
		switch (lastState) {
			case $jmri.turnoutTHROWN:
				$jmri.setJMRI('turnout', name, {"state":$jmri.turnoutCLOSED}, "post");
				break;
			case $jmri.turnoutCLOSED:
				$jmri.setJMRI('turnout', name, {"state":$jmri.turnoutTHROWN}, "post");
				break;
			default:
				$jmri.setJMRI('turnout', name, {"state":$jmri.turnoutCLOSED}, "post");
				break;
		}
	} else {	// 0(unknown) 2(Active) 4(Inactive) 8(inconsistent) - Can only activate
		$jmri.setJMRI('route', name, {"state":$jmri.routeACTIVE}, "post");
	}
};

//----------------------------------------- Open panel - Click (mouse and touch with simulation)
var openPanel = function(e, name, URL) {
	if (!isLeftButton(e)) return;
	e.preventDefault();
	e.stopImmediatePropagation();
	// URL not used for now
	var url = document.URL.split('?')[0] + '?panelname=' + encodeURIComponent(name);
	if ($toFrame) {
		if (addToFrameList(url + '&inframe')) window.open(document.URL.split('?')[0], $pageInFrame ? '_top' : '_self');
	} else window.open(url, url);
};

//----------------------------------------- Select item from list - Click (mouse and touch with simulation)
var itemSelected = function(e, f) {
	if (!isLeftButton(e)) return;
	e.preventDefault();
	e.stopImmediatePropagation();
	var o = $(e.currentTarget);
	var i = o.attr('seq');
	o.parent().remove();
	if (i >= 0) f(i);
};

//----------------------------------------- Remove alert message - Click (mouse and touch with simulation)
var removeSmoothAlertMouse = function(e) {
	if (!isLeftButton(e)) return;
	e.preventDefault();
	e.stopImmediatePropagation();
	var o = $(e.currentTarget);
	if (o.attr('preventClick') !== undefined) return;
	o.remove();
};

//----------------------------------------- Initial position for moving object (touch)
var startMoveTouch = function(e, o) {startMove(e, o, e.targetTouches[0]);};

//----------------------------------------- Initial position for moving object (mouse)
var startMoveMouse = function(e) {if (isLeftButton(e)) startMove(e, $(e.currentTarget), e);};

//----------------------------------------- Initial position for moving object
var startMove = function(e, o, t) {
	e.preventDefault();
	e.stopImmediatePropagation();
	o.removeAttr('preventClick');
	o.attr('y', t.pageY);
	o.attr('x', t.pageX);
	o.attr('cursor', o.css('cursor'));
	o.css('cursor', 'move');
};

//----------------------------------------- Several steps in moving object (touch)
var movingTouch = function(e, o) {moving(e, o, e.targetTouches[0]);};

//----------------------------------------- Several steps in moving object (mouse)
var movingMouse = function(e) {if (isLeftButton(e)) moving(e, $(e.currentTarget), e);};

//----------------------------------------- Several steps in moving object
var moving = function(e, o, t) {
	e.preventDefault();
	e.stopImmediatePropagation();
	if (o.attr('y') === undefined || o.attr('x') === undefined || o.attr('cursor') === undefined) return;
	o.attr('preventClick', '');
	var deltaY = t.pageY - o.attr('y');
	var deltaX = t.pageX - o.attr('x');
	o.css('top', Number(o.css('top').split('px')[0]) + deltaY);
	o.css('left', Number(o.css('left').split('px')[0]) + deltaX);
	o.attr('y', t.pageY);
	o.attr('x', t.pageX);
};

//----------------------------------------- Final position for moving object (touch)
var stopMoveTouch = function(e, o) {stopMove(e, o);};

//----------------------------------------- Final position for moving object (mouse)
var stopMoveMouse = function(e) {if (isLeftButton(e)) stopMove(e, $(e.currentTarget));};

//----------------------------------------- Final position for moving object
var stopMove = function(e, o) {
	e.preventDefault();
	e.stopImmediatePropagation();
	if (o.attr('preventClick') !== undefined) {
		o.css('cursor', o.attr('cursor'));
		o.removeAttr('y');
		o.removeAttr('x');
		o.removeAttr('cursor');
	} else o.remove();
};

//----------------------------------------- Final position for moving object, if out of parent (mouse)
var stopMoveConditionallyMouse = function(e) {
	if (!isLeftButton(e)) return;
	e.preventDefault();
	e.stopImmediatePropagation();
	var o = $(e.currentTarget);
	var mouseY = e.pageY;
	var mouseX = e.pageX;
	var oTop = o.offset().top;
	var oLeft = o.offset().left;
	var oBottom = oTop + o.outerHeight(false);
	var oRight = oLeft + o.outerWidth(false);
	if (mouseY > oTop && mouseY < oBottom && mouseX > oLeft && mouseX < oRight) return;
	o.css('cursor', o.attr('cursor'));
	o.removeAttr('y');
	o.removeAttr('x');
	o.removeAttr('cursor');
};

//----------------------------------------- Manage new throttles - Click (mouse and touch with simulation)
var manageNewThrottles = function(e) {
	if (!isLeftButton(e)) return;
	e.preventDefault();
	e.stopImmediatePropagation();
	var toFrame = $('#toFrame');
	var toFrameText = toFrame.children('.buttonText');
	if ($toFrame) {
		$toFrame = false;
		toFrameText.text('single');
	} else {
		$toFrame = true;
		toFrameText.text('multi');
	}
	var bc = toFrame.css('background-color');
	toFrame.css('background-color', toFrame.css('color'));
	toFrame.css('color', bc);
};

//----------------------------------------- Manage speed control position - Click (mouse and touch with simulation)
var manageSpeedPosition = function(e) {
	if (!isLeftButton(e)) return;
	e.preventDefault();
	e.stopImmediatePropagation();
	changeSpeedPosition(!$speedPosition);
	saveLocalInfo('webThrottle.speedPosition', $speedPosition);
};

//----------------------------------------- Change speed control position
var changeSpeedPosition = function(speedPos) {
	var speedPosition = $('#speedPosition');
	if (!speedPosition.length) return;
	var speedPositionText = speedPosition.children('.buttonText');
	if (!speedPos) {
		$speedPosition = false;
		speedPositionText.text('|::');
	} else {
		$speedPosition = true;
		speedPositionText.text('::|');
	}
	$viewportHeight = 0;	// Force rezise
};

//----------------------------------------- Send immediate STOP - Click (mouse and touch with simulation)
var immediateStop = function(e) {
	if (!isLeftButton(e)) return;
	e.preventDefault();
	e.stopImmediatePropagation();
	if ($('body').attr('locoReady') != 'true') return;
	$jmri.setJMRI('throttle', $locoAddress, {"speed":$jmri.EMERGENCY_STOP});
};

//----------------------------------------- Select another loco - Click (mouse and touch with simulation)
var changeLoco = function(e) {
	if (!isLeftButton(e)) return;
	e.preventDefault();
	e.stopImmediatePropagation();
	var o = $(e.currentTarget);
	$locoList = [];
	var rg = loadLocalInfo('webThrottle.rosterGroup');
	if (rg || rg == '') $rosterGroup = rg; else saveLocalInfo('webThrottle.rosterGroup', $rosterGroup);
	if ($rosterGroup == '[Panels]') $rosterGroup = '';
	var roster = $jmri.getRoster($rosterGroup);
	if (roster.length) roster.forEach(function(loco) {$locoList.push(loco.name + ' (' + loco.dccAddress + ')');});
	var f = function(i) {
		var url = document.URL.split('?')[0] + '?loconame=' + encodeURIComponent($locoList[i].substring(0, $locoList[i].lastIndexOf(' ')));
		if ($inFrame) {
			if (replaceInFrameList(document.URL, url + '&inframe')) window.parent.$('#' + encodeId(document.URL)).attr('src', url + '&inframe').attr('id', encodeId(url + '&inframe'));
		}
		else if (document.URL != url) window.location = url;
	};
	var ai = 0;
	for (var i = 0; i < $locoList.length; i++) if ($locoList[i] == $('#locoName').text()) ai = i;
	selectionList($locoList, 'Locos', ai, f);
};

//----------------------------------------- Set direction - Click (mouse and touch with simulation)
var setDirection = function(e, forward) {
	if (!isLeftButton(e)) return;
	e.preventDefault();
	e.stopImmediatePropagation();
	if ($('body').attr('locoReady') != 'true') return;
	$jmri.setJMRI('throttle', $locoAddress, {"forward": forward ? $jmri.TRUE : $jmri.FALSE});
};

//----------------------------------------- Manage tilt to control speed or not - Click (mouse and touch with simulation)
var manageMovementActive = function(e) {
	if (!isLeftButton(e)) return;
	e.preventDefault();
	e.stopImmediatePropagation();
	changeMovementActive(!$movementActive);
	saveLocalInfo('webThrottle.movementActive', $movementActive);
};

//----------------------------------------- Change tilt to control speed or not
var changeMovementActive = function(movActive) {
	var movementActive = $('#movementActive');
	if (!movementActive.length) return;
	var movementActiveText = movementActive.children('.buttonText');
	var speedTouch = $('#speedTouch');
	speedTouch.off('touchstart');
	speedTouch.off('touchmove');
	speedTouch.off('touchend');
	speedTouch.off('mousedown');
	speedTouch.off('mousemove');
	speedTouch.off('mouseup');
	speedTouch.off('mouseout');
	if (!movActive) {
		$movementActive = false;
		movementActiveText.text('normal');
		if ($hasTouch) {
			speedTouch.on('touchstart', function(event) {speedPressedTouch(event.originalEvent);});
			speedTouch.on('touchmove', function(event) {speedMovingTouch(event.originalEvent);});
			speedTouch.on('touchend', function(event) {speedReleasedTouch(event.originalEvent);});
		} else {
			speedTouch.on('mousedown', function(event) {speedPressedMouse(event);});
			speedTouch.on('mousemove', function(event) {speedMovingMouse(event);});
			speedTouch.on('mouseup', function(event) {speedReleasedMouse(event);});
			speedTouch.on('mouseout', function(event) {speedCanceledMouse(event);});
		}
	} else {
		$movementActive = true;
		movementActiveText.text('tilt');
		speedTouch.on('touchstart', function(event) {speedPressedTiltTouch(event.originalEvent);});
		speedTouch.on('touchend', function(event) {speedReleasedTiltTouch(event.originalEvent);});
	}
};

//----------------------------------------- Orientation changed (device)
var deviceOrientation = function(e) {
	if ($('body').attr('locoReady') != 'true') return;
	if ($movementActive && $movementOn) {
		if ($orientation != window.top.orientation) $movementTilt = null;
		$orientation = window.top.orientation;
		var m;
		switch ($orientation) {
			case 0:
				m = e.beta;
				break;
			case 90:
				m = -e.gamma;
				break;
			case -90:
				m = e.gamma;
				break;
			default:
				m = -e.beta;
				break;
		}
		m = Math.round(m * $speedStep * 2);	// Sensitivity (for $speedStep=10%): 5º
		if ($movementTilt != m) {
			if ($movementTilt != null) {
				$movementCtrl = m - $movementTilt;
				var speed = $('.speed');
				var speedValue = Number(speed.attr('speed'));
				if (speedValue < 0) speedValue = 0;
				speedValue+= ($movementCtrl * $speedStep / 2);	// 100º <=> full cursor displacement
				if (speedValue < 0) speedValue = 0;
				if (speedValue > 1) speedValue = 1;
				speed.attr('speed', speedValue);
				showSpeed();
				var speedValueFormated = '' + speedValue;
				if (speedValue == 0) speedValueFormated = $jmri.STOP;
				if (speedValue == 1) speedValueFormated = $jmri.FULL_SPEED;
				$jmri.setJMRI('throttle', $locoAddress, {"speed":speedValueFormated});
			}
			$movementTilt = m;
		}
	}
}

//----------------------------------------- Speed zone pressed - tilt (touch)
var speedPressedTiltTouch = function(e) {
	e.preventDefault();
	e.stopImmediatePropagation();
	if ($('body').attr('locoReady') != 'true') return;
	$movementTilt = null;
	$movementOn = true;
	$speedFeedback = false;
};

//----------------------------------------- Speed zone released - tilt (touch)
var speedReleasedTiltTouch = function(e) {
	e.preventDefault();
	e.stopImmediatePropagation();
	if ($('body').attr('locoReady') != 'true') return;
	$movementOn = false;
	$speedFeedback = true;
	throttleSpeed($locoAddress, $speedAux);
};

//----------------------------------------- Speed zone pressed (touch)
var speedPressedTouch = function(e) {speedPressed(e, e.targetTouches[0]);};

//----------------------------------------- Speed zone pressed (mouse)
var speedPressedMouse = function(e) {if (isLeftButton(e)) speedPressed(e, e);};

//----------------------------------------- Speed zone pressed
var speedPressed = function(e, t) {
	e.preventDefault();
	e.stopImmediatePropagation();
	if ($('body').attr('locoReady') != 'true') return;
	$movementCtrl = t.pageY;
	functionForSpeedCtrl();
	$speedTimer = setInterval(function() {functionForSpeedCtrl();}, $speedInterval);
	$speedFeedback = false;
};

//----------------------------------------- Function for speed control
var functionForSpeedCtrl = function() {
	var speed = $('.speed');
	var speedValue = Number(speed.attr('speed'));
	if (speedValue < 0) speedValue = 0;
	var speedLimitsHeight = $('#speedLimits').height();
	var speedBarValue = $('#speedBar').offset().top;
	var increment = (speedBarValue - $movementCtrl) / speedLimitsHeight;
	var maxIncrement = speedLimitsHeight * $speedStep;
	speedValue+= (increment > $speedStep ? $speedStep : (increment < $speedStep * -1 ? $speedStep * -1 : increment));
	if (speedValue < 0) speedValue = 0;
	if (speedValue > 1) speedValue = 1;
	speed.attr('speed', speedValue);
	showSpeed();
	var speedValueFormated = '' + speedValue;
	if (speedValue == 0) speedValueFormated = $jmri.STOP;
	if (speedValue == 1) speedValueFormated = $jmri.FULL_SPEED;
	$jmri.setJMRI('throttle', $locoAddress, {"speed":speedValueFormated});
};

//----------------------------------------- Speed zone moving (touch)
var speedMovingTouch = function(e) {speedMoving(e, e.targetTouches[0]);};

//----------------------------------------- Speed zone moving (mouse)
var speedMovingMouse = function(e) {if (isLeftButton(e)) speedMoving(e, e);};

//----------------------------------------- Speed zone moving
var speedMoving = function(e, t) {
	e.preventDefault();
	e.stopImmediatePropagation();
	if ($('body').attr('locoReady') != 'true') return;
	$movementCtrl = t.pageY;
};

//----------------------------------------- Speed zone released (touch)
var speedReleasedTouch = function(e) {speedReleased(e);};

//----------------------------------------- Speed zone released (mouse)
var speedReleasedMouse = function(e) {if (isLeftButton(e)) speedReleased(e);};

//----------------------------------------- Speed zone canceled (mouse)
var speedCanceledMouse = function(e) {if (isLeftButton(e)) speedReleased(e);};

//----------------------------------------- Speed zone released or canceled
var speedReleased = function(e) {
	e.preventDefault();
	e.stopImmediatePropagation();
	if ($('body').attr('locoReady') != 'true') return;
	if ($speedTimer) {
		clearInterval($speedTimer);
		$speedTimer = null;
	}
	$speedFeedback = true;
	throttleSpeed($locoAddress, $speedAux);
};

//----------------------------------------- Function button click - Click (mouse and touch with simulation)
var functionClick = function(e) {
	if (!isLeftButton(e)) return;
	e.preventDefault();
	e.stopImmediatePropagation();
	if ($('body').attr('locoReady') != 'true') return;
	var o = $(e.currentTarget);
	var lastState = o.attr('state');
	var propName = 'F' + o.attr('id').substr(12);	// id=locoFunction<n>
	var obj  = {};
	obj[propName] = (lastState == '' + $jmri.TRUE) ? $jmri.FALSE : $jmri.TRUE;
	$jmri.setJMRI('throttle', $locoAddress, obj);
};

//----------------------------------------- Function button pressed (touch)
var functionPressedTouch = function(e, o) {functionPressed(e, o);};

//----------------------------------------- Function button pressed (mouse)
var functionPressedMouse = function(e) {if (isLeftButton(e)) functionPressed(e, $(e.currentTarget));};

//----------------------------------------- Function button pressed
var functionPressed = function(e, o) {
	e.preventDefault();
	e.stopImmediatePropagation();
	if ($('body').attr('locoReady') != 'true') return;
	var propName = 'F' + o.attr('id').substr(12);	// id=locoFunction<n>
	var obj  = {};
	obj[propName] = $jmri.TRUE;
	$jmri.setJMRI('throttle', $locoAddress, obj);
};

//----------------------------------------- Function button released (touch)
var functionReleasedTouch = function(e, o) {functionReleased(e, o);};

//----------------------------------------- Function button released (mouse)
var functionReleasedMouse = function(e) {if (isLeftButton(e)) functionReleased(e, $(e.currentTarget));};

//----------------------------------------- Function button released
var functionReleased = function(e, o) {
	e.preventDefault();
	e.stopImmediatePropagation();
	if ($('body').attr('locoReady') != 'true') return;
	var propName = 'F' + o.attr('id').substr(12);	// id=locoFunction<n>
	var obj  = {};
	obj[propName] = $jmri.FALSE;
	$jmri.setJMRI('throttle', $locoAddress, obj);
};

//----------------------------------------- Function button released or canceled (mouse)
var functionCanceledMouse = function(e) {
	if (!isLeftButton(e)) return;
	e.preventDefault();
	e.stopImmediatePropagation();
	var o = $(e.currentTarget);
	var mouseY = e.pageY;
	var mouseX = e.pageX;
	var oTop = o.offset().top;
	var oLeft = o.offset().left;
	var oBottom = oTop + o.outerHeight(false);
	var oRight = oLeft + o.outerWidth(false);
	if (mouseY > oTop && mouseY < oBottom && mouseX > oLeft && mouseX < oRight) return;
	if ($('body').attr('locoReady') != 'true') return;
	var propName = 'F' + o.attr('id').substr(12);	// id=locoFunction<n>
	var obj  = {};
	obj[propName] = $jmri.FALSE;
	$jmri.setJMRI('throttle', $locoAddress, obj);
};

//===================================================================================== Generic functions =============================

//----------------------------------------- Debug print
// Use the following lines anywhere logs or alerts are needed:
// $debug && window.console && console.log(anything);
// $debug && window.console && console.log(new Date() + ' - ' + document.title + '\n' + text);
// $debug && alert(text);
// $debug && alert(new Date() + '\n\n' + document.title + '\n\n' + text);

//----------------------------------------- Get URL parameters (parameters are not case sensitive so, 'key' must be compared in lowercase)
var getUrlParameters = function() {			//Item 'undefined' if index not found
	var vars = [], hash, key;
	var hashes;
	var auxInt = window.location.href.indexOf('?');
	if (auxInt < 0) return vars;
	hashes = window.location.href.slice(auxInt + 1).split('&');
	for (var i = 0; i < hashes.length; i++) {
		if (hashes[i].indexOf('=') == -1) hashes[i]+= '=';
		hash = hashes[i].split('=');
		key = hash[0].toLowerCase();
		vars.push(decodeURIComponent(key));
		vars[key] = decodeURIComponent(hash[1]).replace(/\+/g, " "); //undo server query string space replacements
	}
	return vars;
};

//----------------------------------------- Save local info ('key' is case sensitive)
var	saveLocalInfo = function(key, value) {
	var _key = encodeURIComponent(key);
	var _value = encodeURIComponent(value);
	localStorage[_key] = _value;
};

//----------------------------------------- Load local info ('key' is case sensitive)
var	loadLocalInfo = function(key) {			//Return 'undefined' if not found
	var _key = encodeURIComponent(key);
	var value;
	value = localStorage[_key];
	return ((value === undefined || value === null) ? (function() {})() : decodeURIComponent(value));
};

//----------------------------------------- Remove local info ('key' is case sensitive)
var	removeLocalInfo = function(key) {
		var _key = encodeURIComponent(key);
		localStorage.removeItem(_key);
};

//----------------------------------------- Set top related to parent content
var setTopFromParentContent = function(o, px) {
	var p = Number(o.parent().css('padding-top').split('px')[0]);
	o.css('top', px + p);
};

//----------------------------------------- Set left related to parent content
var setLeftFromParentContent = function(o, px) {
	var p = Number(o.parent().css('padding-left').split('px')[0]);
	o.css('left', px + p);
};

//----------------------------------------- Set outer height
var setOuterHeight = function(o, px, includeMargin) {
	var p = includeMargin ? (Number(o.css('margin-top').split('px')[0]) + Number(o.css('margin-bottom').split('px')[0])) : 0;
	p+= Number(o.css('border-top-width').split('px')[0]) + Number(o.css('border-bottom-width').split('px')[0]);
	p+= Number(o.css('padding-top').split('px')[0]) + Number(o.css('padding-bottom').split('px')[0]);
	o.height(px - p);
};

//----------------------------------------- Set outer width
var setOuterWidth = function(o, px, includeMargin) {
	var p = includeMargin ? (Number(o.css('margin-left').split('px')[0]) + Number(o.css('margin-right').split('px')[0])) : 0;
	p+= Number(o.css('border-left-width').split('px')[0]) + Number(o.css('border-right-width').split('px')[0]);
	p+= Number(o.css('padding-left').split('px')[0]) + Number(o.css('padding-right').split('px')[0]);
	o.width(px - p);
};

//----------------------------------------- Show a list to select an item (-1 if canceled)
var selectionList = function(a, t, ai, f) {
	if ($('.selectionList').length) return;	// Nothing if a list is already active
	var o = $('<div>');
	var item;
	o.addClass('selectionList');
	$('body').append(o);
	item = $('<p>');
	item.addClass('selectionListItem');
	item.attr('id', 'selectionListTitle');
	o.append(item);
	item.text(t);
	item.html(item.html().replace(/ /g, '&nbsp;'));
	for (var i = 0; i < a.length; i++) {
		item = $('<p>');
		item.attr('seq', i);
		item.addClass('selectionListItem');
		o.append(item);
		if (a[i] != '') {
			item.on('click', function(event) {itemSelected(event, f);});
			item.text(a[i]);
		} else {
			item.attr('id', 'selectionListItemBlank');
			item.text(' ');
		}
		item.html(item.html().replace(/ /g, '&nbsp;'));
	}
	if (ai >= 0) $('.selectionListItem[seq = ' + ai + ']').addClass('activeListItem');
	item = $('<p>');
	item.addClass('selectionListItem');
	item.attr('id', 'selectionListItemBlank');
	o.append(item);
	item.html('&nbsp;');
	item = $('<p>');
	item.attr('seq', -1);
	item.on('click', function(event) {itemSelected(event, f);});
	item.addClass('selectionListItem');
	item.attr('id', 'selectionListCancel');
	o.append(item);
	item.text('Cancel');
	resizeSelectionList();
};

//----------------------------------------- Show alert message (no blocking) - timeout parameter 'seconds' is optional
var smoothAlert = function(message, seconds) {
	var lines = ('' + message).split('\n');
	var o = $('<div>');
	if ($hasTouch) {
		o.on('touchstart', function(event) {startMoveTouch(event.originalEvent, $(this));});
		o.on('touchmove', function(event) {movingTouch(event.originalEvent, $(this));});
		o.on('touchend', function(event) {stopMoveTouch(event.originalEvent, $(this));});
	} else {
		o.on('click', function(event) {removeSmoothAlertMouse(event);});
		o.on('mousedown', function(event) {startMoveMouse(event);});
		o.on('mousemove', function(event) {movingMouse(event);});
		o.on('mouseup', function(event) {stopMoveMouse(event);});
		o.on('mouseout', function(event) {stopMoveConditionallyMouse(event);});
	}
	o.addClass('smoothAlert');
	$('body').append(o);
	lines.forEach(function(line) {
		var l = $('<p>');
		l.addClass('smoothAlertLine');
		o.append(l);
		l.text(line + ((line.length) ? '' : ' '));
		l.html(l.html().replace(/ /g, '&nbsp;'));
	});
	o.css('z-index', $zIndexForSmoothAlert--);
	o.addClass('fadeIn');
	if (!isNaN(seconds) && seconds > 0) o.attr('seconds', seconds);
	resizeSmoothAlerts();
};

//----------------------------------------- Encode text to be used in attribute 'id'
var	encodeId = function(text) {
	var s = escape(text);	// will insert: %		The exceptions: * @ - _ + . /
	s = s.replace(/\%/g, '_-0-_');
	s = s.replace(/\*/g, '_-1-_');
	s = s.replace(/\@/g, '_-2-_');
	s = s.replace(/\+/g, '_-3-_');
	s = s.replace(/\./g, '_-4-_');
	s = s.replace(/\//g, '_-5-_');
	return s;
};

//----------------------------------------- Get width of vertical scrollbar
var getVerticalScrollbarWidth = function() {
	var p, c, w;
	p = $('<div style="width:50px;height:50px;overflow:auto"><div/></div>').appendTo('body');
	c = p.children();
	w = c.innerWidth() - c.height(99).innerWidth();
	p.remove();
	return w;
}
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ End of file
