Source: webAppUtils.js

/**
 * @file
 * This Javascript library provides functions that simplify the creation of web-based user interfaces interacting with an AsTeRICS model.
 * The lib provides functions for downloading files from a webserver, widget <-> model synchronization, up/download/start models, setting/getting properties of deployed model.
 *   
 * @requires jquery-3.2.1.min.js (or maybe lower versions also)
 * @requires JSmap.js (AsTeRICS 3.0)
 * @requires areCommunicator.js (AsTeRICS 3.0)
 * 
 * @author Martin Deinhofer
 * @version 0.1
 */

/**
 * Loads a file hosted on the same webserver as this file and returns the contents as plain text.
 * @param {string} relFilePath - The path to the file relative to http://<location.origin>/subpath/.
 * @param {function(fileContentsAsString)} [successCallback=defaultSuccessCallback] - The callback function to be called with the file contents. 
 * @param {function(HTTPstatus, errorMessage)} [errorCallback=defaultErrorCallback]- Callback function in case of an error.
 */
function loadFileFromWebServer(relFilePath, successCallback, errorCallback) {
	//assign default callback functions if none was provided.
	successCallback=getSuccessCallback(successCallback);
	errorCallback=getErrorCallback(errorCallback);	
	
	var httpReq = new XMLHttpRequest();
	console.log("location.origin: "+location.origin);
	
	var pathFix='';
	if(Object.is(location.origin, 'https://asterics.github.io')||Object.is(location.origin, 'http://asterics.github.io')) {
		pathFix='/AsTeRICS';
		console.log("Adding pathFix: "+pathFix);
	}
	relFilePath=location.origin+pathFix+'/'+relFilePath;
	console.log('Fetching file from webserver: '+relFilePath);
	
	/*
	//With jQuery you could use something like this to fetch the file easily, nevertheless to be independent we use the hard approach.
	$.get(relFilePath).then(function(response) {
		successCallback(response);
	});*/

	httpReq.onreadystatechange = function() {
		if (httpReq.readyState === XMLHttpRequest.DONE && (httpReq.status === 404 || httpReq.status === 0)) {						
			alert('Could not find requested file: '+relFilePath);
			
		} else if (httpReq.readyState === XMLHttpRequest.DONE && httpReq.status === 200) {
			//success, so call success-callback
			console.log('File from Webserver successfully loaded: '+relFilePath);
			successCallback(httpReq.responseText);
		}
	}	
	httpReq.open('GET', relFilePath, true);
	httpReq.send();
}

/**
 * Deploys a model file hosted on the same webserver as this file to a running ARE instance e.g. on localhost.
 * @param {string} relFilePath - The path to the file relative to http://location.origin/subpath/.
 * @param {function(data, HTTPstatus)} [successCallback=defaultSuccessCallback] - The callback function to be called with the file contents. 
 * @param {function(HTTPstatus, errorMessage)} [errorCallback=defaultErrorCallback]- Callback function in case of an error. 
 */
function deployModelFromWebserver(relFilePath, successCallback, errorCallback) {
	//assign default callback functions if none was provided.
	successCallback=getSuccessCallback(successCallback);
	errorCallback=getErrorCallback(errorCallback);	
	
	loadFileFromWebServer(relFilePath, 
		function(modelInXML) {				
			uploadModel(successCallback, errorCallback, modelInXML);
		});
}

/**
 * Deploys a model file hosted on the same webserver as this file to a running ARE instance e.g. on localhost.
 * Additionally applies the property settings in the given propertyMap and if successful starts the model.
 * @param {string} relFilePath - The path to the file relative to http://location.origin/subpath/.
 * @param {string } propertyMap - A JSON string of property keys and values (see function setRuntimeComponentProperties) in the format: 
 
 {
   "Component_id_1":{
      "key_1_1":"val_1_1",
      "key_1_2":"val_1_2"
   },
   "Component_id_2":{
      "key_2_1":"val_2_1",
      "key_2_2":"val_2_2"
   }
}
 * @param {function(data, HTTPstatus)} [successCallback=defaultSuccessCallback] - The callback function to be called with the file contents. 
 * @param {function(HTTPstatus, errorMessage)} [errorCallback=defaultErrorCallback]- Callback function in case of an error. 
 */
function deployModelFromWebserverApplySettingsAndStartModel(relFilePath, propertyMap, successCallback, errorCallback) {
	//assign default callback functions if none was provided.
	successCallback=getSuccessCallback(successCallback);
	errorCallback=getErrorCallback(errorCallback);	
	
	deployModelFromWebserver(relFilePath,
		function() {
			setRuntimeComponentProperties(			
				function (data, HTTPstatus){
					if(JSON.parse(data).length == 0) {
						var errorMsg="The property settings could not be applied.";
						alert(errorMsg);
					}
					console.log('The following properties could be set: '+data);
					
					startModel(successCallback,errorCallback);
				}, 
				errorCallback, propertyMap);
		});
}

/**
 * Stores the file hosted on the same webserver as this file at a running ARE instance e.g. on localhost and the given relFilePathARE.
 * @param {string} relFilePath - The path of the file relative to http://location.origin/subpath/.
 * @param {string} relFilePathARE - The store path of the file on the ARE relative to ARE/data.
 * @param {function(data, HTTPstatus)} [successCallback=defaultSuccessCallback] - The callback function to be called with the file contents. 
 * @param {function(HTTPstatus, errorMessage)} [errorCallback=defaultErrorCallback]- Callback function in case of an error. 
 */
function storeFileFromWebserverOnARE(relFilePath, relFilePathARE, successCallback, errorCallback) {
	//assign default callback functions if none was provided.
	successCallback=getSuccessCallback(successCallback);
	errorCallback=getErrorCallback(errorCallback);	
	
	loadFileFromWebServer(relFilePath, 
		function(fileContentsAsString) {
			storeData(successCallback, errorCallback, relFilePathARE, fileContentsAsString);
		});
}

/**
 * Download the model file (modelFilePathOnWebserver) hosted on a web server, apply all settings to the XML model, 
 * which have a defined binding (data-asterics-model-binding-1,...) to a model property and finally start the model.
 * @param {string} modelFilePathOnWebserver - The path of the file relative to http://location.origin/subpath/.
 * @param {function(data, HTTPstatus)} [successCallback=defaultSuccessCallback] - The callback function to be called with the file contents. 
 * @param {function(HTTPstatus, errorMessage)} [errorCallback=defaultErrorCallback]- Callback function in case of an error.   
*/		
function applySettingsInXMLModelAndStart(modelFilePathOnWebserver, successCallback, errorCallback) {
	//assign default callback functions if none was provided.
	successCallback=getSuccessCallback(successCallback);
	errorCallback=getErrorCallback(errorCallback);	
	
	loadFileFromWebServer(modelFilePathOnWebserver, function(modelInXML){
		modelInXML=updateModelPropertiesFromWidgets(modelInXML);
		
		//Finally upload and start modified model.
		uploadModel(function(data, HTTPstatus) {					
			startModel(function(data,HTTPStatus) {
				successCallback(data, HTTPStatus);
			},errorCallback);					
		}, errorCallback, modelInXML);
	});	
}

/**
 * Download the currently deployed model from the ARE and update all widgets with the property values in the XML model, 
 * which have a defined binding (data-asterics-model-binding-1,...) to a model property.
 * @param {string} modelFilePathOnWebserver - The path of the file relative to http://location.origin/subpath/.
 * @param {function(data, HTTPstatus)} [successCallback=defaultSuccessCallback] - The callback function to be called with the file contents. 
 * @param {function(HTTPstatus, errorMessage)} [errorCallback=defaultErrorCallback]- Callback function in case of an error.   
*/		
function updateWidgetsFromDeployedModel(successCallback, errorCallback) {
	//assign default callback functions if none was provided.
	successCallback=getSuccessCallback(successCallback);
	errorCallback=getErrorCallback(errorCallback);

	downloadDeployedModel(function(data, HTTPStatus) {
		//TODO: Check if the modelName is identical to the modelName of the template model, otherwise we should not
		//update the widgets from a wrong model.
		updateWidgetsFromModelProperties(data);
	},successCallback);
}

/**
 * Applies the settings to the model modelFilePathOnWebserver and saves it as autostart model on the ARE.
 * @param {string} modelFilePathOnWebserver - The path of the file relative to http://location.origin/subpath/.
 * @param {function(data, HTTPstatus)} [successCallback=defaultSuccessCallback] - The callback function to be called with the file contents. 
 * @param {function(HTTPstatus, errorMessage)} [errorCallback=defaultErrorCallback]- Callback function in case of an error.   
*/		
function saveSettingsAsAutostartModel(modelFilePathOnWebserver, successCallback, errorCallback) {
	//assign default callback functions if none was provided.
	if(typeof successCallback !== 'function') {
		successCallback=function(data,HTTPStatus) {
			alert('Successfully saved settings as autostart!');
		}
	}
	errorCallback=getErrorCallback(errorCallback);
	
	loadFileFromWebServer(modelFilePathOnWebserver, function(modelInXML){
		modelInXML=updateModelPropertiesFromWidgets(modelInXML);
		storeModel(successCallback,errorCallback,'autostart.acs',modelInXML);
	});			
}

/** 
 * @const {string}
 * @description HTML5 data attribute to define binding to AsTeRICS model 
 */
var dataAttrAstericsModelBinding="data-asterics-model-binding-1";

/**
 * Updates the property values of the given modelInXML string with the currently set widget values.
 * @param {string} modelInXML - The AsTeRICS model as XML string.
 * @returns {string} The modified XML model as string.
 */
function updateModelPropertiesFromWidgets(modelInXML) {
	widgetIdToPropertyKeyMap=generateWidgetIdToPropertyKeyMap();
	console.log("Updating "+widgetIdToPropertyKeyMap.length+" widgets from model properties.");
	
	//parse template modelInXML to document object
	var xmlDoc = $.parseXML( modelInXML );
	
	//Update property values with values of input widgets.
	for(var i=0;i<widgetIdToPropertyKeyMap.length;i++) {
		var widgetVal=$(widgetIdToPropertyKeyMap[i]["widgetId"]).val();
		if (typeof widgetVal != 'undefined') {
			console.log("Updating modelProperty <"+widgetIdToPropertyKeyMap[i]["componentKey"]+"."+widgetIdToPropertyKeyMap[i]["propertyKey"]+"="+widgetVal+">");
			setPropertyValueInXMLModel(widgetIdToPropertyKeyMap[i]["componentKey"],widgetIdToPropertyKeyMap[i]["propertyKey"],widgetVal,xmlDoc);
		} else {
			console.log("widgetId <"+widgetIdToPropertyKeyMap[i]["widgetId"]+"=undefined>");
		}
	}
	
	//Convert back XML document to XML string.
	modelInXML=xmlToString(xmlDoc);
	return modelInXML;
}

/**
 * Updates the widgets (HTML input elements) with the property values of the given modelInXML string.
 * @param {string} modelInXML - The AsTeRICS model as XML string.
 */
function updateWidgetsFromModelProperties(modelInXML) {
	widgetIdToPropertyKeyMap=generateWidgetIdToPropertyKeyMap();
	console.log("Updating "+widgetIdToPropertyKeyMap.length+" widgets from model properties.");
	
	//parse template modelInXML to document object
	var xmlDoc = $.parseXML( modelInXML );
	
	//Update property values with values of input widgets.
	for(var i=0;i<widgetIdToPropertyKeyMap.length;i++) {
		var propVal=getPropertyValueFromXMLModel(widgetIdToPropertyKeyMap[i]["componentKey"],widgetIdToPropertyKeyMap[i]["propertyKey"],xmlDoc);
		if(typeof propVal != 'undefined') {
			console.log("Updating widget <"+widgetIdToPropertyKeyMap[i]["widgetId"]+"="+propVal+">");
			$(widgetIdToPropertyKeyMap[i]["widgetId"]).val(propVal);					
		}
	}	
}

/**
 * Generates an array describing the bindings between all widgetIds (id of HTML5 input tag) and their respective model properties.
 * @returns {Array} - Array with Javascript object elements. 
 */
function generateWidgetIdToPropertyKeyMap() {
	var widgetIdToPropertyKeyMap=[];
	var widgetList=$("["+dataAttrAstericsModelBinding+"]");
	for(var i=0;i<widgetList.length;i++) {
		var bindings=$(widgetList[i]).data();
		for(binding in bindings) {
			var bindingObj=	{
				widgetId:"#"+$(widgetList[i]).attr('id'),
				componentKey:bindings[binding]["componentKey"],
				propertyKey:bindings[binding]["propertyKey"]
			}
			widgetIdToPropertyKeyMap.push(bindingObj);
		}
	}
	return widgetIdToPropertyKeyMap;
}

/**
 * Returns a valid callback function - either successCallback if != undefined or {defaultSuccessCallback}.
 * @param {function(data, HTTPstatus)} [successCallback=defaultSuccessCallback] - The callback function to be used.
 * @returns {function(data, HTTPstatus)} - Either successCallback or defaultSuccessCallback.
*/		
function getSuccessCallback(successCallback) {
	if(typeof successCallback !== 'function') {
		return defaultSuccessCallback;
	}
	return successCallback;	
}

/**
 * Returns a valid callback function - either errorCallback if != undefined or {defaultErrorCallback}.
 * @param {function(HTTPstatus, errorMessage)} [errorCallback=defaultErrorCallback]- The callback function to be used.
 * @returns {function(HTTPstatus, errorMessage)} - Either errorCallback or defaultErrorCallback.
*/		
function getErrorCallback(errorCallback) {
	if(typeof errorCallback !== 'function') {
		return defaultErrorCallback;
	}
	return errorCallback;
}


/* generic callback handler */
/**
 * This is the default success callback. 
 * By default nothing is done.
 *
 * @callback defaultSuccessCallback
 * @param {data} - response text or message.
 * @param {HTTPstatus} - HTTP status code if applicable.
 */
function defaultSuccessCallback(data, HTTPstatus) {}
/**
 * This is the default error callback. 
 * By default an error dialog (alert) is opened.
 *
 * @callback defaultErrorCallback
 * @param {HTTPstatus} - HTTP status code if applicable.
 * @param {errorMessage} - The error message to be shown.

 */
function defaultErrorCallback(HTTPstatus, errorMessage) { alert("An error occured: "+errorMessage+"\nPlease ensure to install AsTeRICS and start the ARE!"); }