Source: bodyRenderer.js

/**
 * Provides rendering of the 3D-scaffold data in the dom of the provided id with models
 * defined in the modelsLoader.
 * @class
 * @param {PJP.ModelsLoader} ModelsLoaderIn - defined in modelsLoade.js, providing locations of files.
 * @param {String} PanelName - Id of the target element to create the  {@link PJP.BodyViewer} on.
 * 
 * @author Alan Wu
 * @returns {PJP.BodyViewer}
 */
PJP.BodyViewer = function(ModelsLoaderIn, PanelName)  {

	var currentScene = undefined;
	var currentSpecies = 'human';
	var bodyScenes = new Array();
	bodyScenes['human'] = undefined;
	bodyScenes['pig'] = undefined;
	bodyScenes['mouse'] = undefined;
	bodyScenes['rat'] = undefined;
	var modelsTransformationMap = new Array();
	var bodyGui;
	var bodyPartsGui;
	var currentHoveredMaterial = undefined;
	var currentSelectedMaterial = undefined;
	// Flag for removing geometry from ZincScene when not visible, thus freeing the memory. Default is false.
	var removeWhenNotVisible = false;
	var UIIsReady = false;
	var organsViewer = undefined;
	var modelsLoader = ModelsLoaderIn;

	
	//Represents each physiological organ systems as folder in the dat.gui.
	var systemGuiFolder = new Array();
	systemGuiFolder["Musculo-skeletal"] = undefined;
	systemGuiFolder["Cardiovascular"] = undefined;
	systemGuiFolder["Respiratory"] = undefined;
	systemGuiFolder["Digestive"] = undefined;
	systemGuiFolder["Skin (integument)"] = undefined;
	systemGuiFolder["Urinary"] = undefined;
	systemGuiFolder["Brain & Central Nervous"] = undefined;
	systemGuiFolder["Immunological"] = undefined;
	systemGuiFolder["Endocrine"] = undefined;
	systemGuiFolder["Female Reproductive"] = undefined;
	systemGuiFolder["Male Reproductive"] = undefined;
	systemGuiFolder["Special sense organs"] = undefined;
	
	//Stores physiological organ systems specific gui settings in dat.gui. 
	var systemPartsGuiControls = new Array();
	systemPartsGuiControls["Musculo-skeletal"] = function() {};
	systemPartsGuiControls["Cardiovascular"] = function() {};
	systemPartsGuiControls["Respiratory"] = function() {};
	systemPartsGuiControls["Digestive"] = function() {};
	systemPartsGuiControls["Skin (integument)"] = function() {};
	systemPartsGuiControls["Urinary"] = function() {};
	systemPartsGuiControls["Brain & Central Nervous"] = function() {};
	systemPartsGuiControls["Immunological"] = function() {};
	systemPartsGuiControls["Endocrine"] = function() {};
	systemPartsGuiControls["Female Reproductive"] = function() {};
	systemPartsGuiControls["Male Reproductive"] = function() {};
	systemPartsGuiControls["Special sense organs"] = function() {};
	
	//Array of settings of the body viewer gui controls.
	var bodyControl = function() {
		  this.Background = [ 255, 255, 255 ]; // RGB array
	};
	
	var _this = this;
	
	//ZincRenderer for this viewer.
	var bodyRenderer = null;
	
	//Following matrices offset data that are offset from their proposed location
	var transformationMatrix = new THREE.Matrix4();
	transformationMatrix.set(0.493844, -0.823957, 0.277871, 30,
										  0.0782172, 0.6760355, 0.92953, -130,
										  -0.866025, -0.437309, 0.242407, 1227,
										  0, 0, 0, 1);
	modelsTransformationMap["heart"] = {
			transformation: transformationMatrix,
			folder: "cardiovascular",
			system: "Cardiovascular"};
	
	transformationMatrix = new THREE.Matrix4();
	transformationMatrix.set(120, 0, 0, 0,
										0, 120, 0, -110,
										0, 0, 120, 1230,
										0, 0, 0, 1);
	modelsTransformationMap["lungs"] = {
			transformation: transformationMatrix,
			folder: "respiratory",
			system: "Respiratory"};
	
	/**
	 * Set the organs viewer this {@link PJP.BodyViewer} fires event to.
	 * 
	 * @param {PJP.OrgansViewer} OrgansViewerIn - target Organs Viewer to fire the event to.
	 */
	this.setOrgansViewer = function(OrgansViewerIn) {
		organsViewer = OrgansViewerIn;
	}
	
	/**
	 * Display the provided name with a tool tip.
	 * 
	 * @param {String} name - String to display
	 * @param {Number} x - windows x coordinates
	 * @param {Number} y - windows y coordinates
	 */
	var showBodyTooltip = function(name, x, y) {
		setToolTipText(name);
		tooltipcontainerElement.style.left = x +"px";
		tooltipcontainerElement.style.top = (y - 20) + "px";
		tipElement.style.visibility = "visible";
		tipElement.style.opacity = 1;
		tiptextElement.style.visibility = "visible";
		tiptextElement.style.opacity = 1;
	}
	
	var hideBodyTooltip = function() {
		tipElement.style.visibility = "hidden";
		tipElement.style.opacity = 0;
		tiptextElement.style.visibility = "hidden";
		tiptextElement.style.opacity = 0;
	}
	
	/**
	 * This callback is triggered when a body part is clicked.
	 * @callback
	 */
	var _pickingBodyCallback = function() {
		return function(intersects, window_x, window_y) {
			var bodyClicked = false;
			for (var i = 0; i < intersects.length; i++) {
				if (intersects[i] !== undefined && (intersects[ i ].object.name !== undefined)) {
					if (!intersects[ i ].object.name.includes("Body")) {
						if (organsViewer)
							organsViewer.loadOrgans(currentSpecies, intersects[ i ].object.userData[0], intersects[ i ].object.name);
						if (currentSelectedMaterial && currentSelectedMaterial != intersects[ i ].object.material) {
							if (currentSelectedMaterial == currentHoveredMaterial)
								currentSelectedMaterial.emissive.setHex(0x0000FF);
							else
								currentSelectedMaterial.emissive.setHex(0x000000);
						}
						currentSelectedMaterial = intersects[ i ].object.material;
						currentSelectedMaterial.emissive.setHex(0x00FF00);
						return;
					} else {
						bodyClicked = true;
					}
				}
			}
			if (bodyClicked && organsViewer) {
				organsViewer.loadOrgans(currentSpecies, "Skin (integument)", "Body");
			}
		}	
	};
	
	/**
	 * This callback is triggered when a body part is hovered over by the mosue.
	 * @callback
	 */
	var _hoverBodyCallback = function() {
		return function(intersects, window_x, window_y) {
			var bodyHovered = false;
			for (var i = 0; i < intersects.length; i++) {
				if (intersects[i] !== undefined && (intersects[ i ].object.name !== undefined)) {
					if (!intersects[ i ].object.name.includes("Body")) {
						document.getElementById("bodyDisplayArea").style.cursor = "pointer";
						showBodyTooltip(intersects[ i ].object.name, window_x, window_y);
						if (currentHoveredMaterial &&
						  intersects[ i ].object.material != currentHoveredMaterial && currentHoveredMaterial != currentSelectedMaterial) {
							currentHoveredMaterial.emissive.setHex(0x000000);
							currentHoveredMaterial.depthFunc = THREE.LessEqualDepth;
						}
						if (intersects[ i ].object.material != currentSelectedMaterial) {
							currentHoveredMaterial = intersects[ i ].object.material;
							currentHoveredMaterial.emissive.setHex(0x0000FF);
						} else {
							currentHoveredMaterial = undefined;
						}
						return;
					} else {
						bodyHovered = true;
					}
				}
			}
			if (currentHoveredMaterial && currentHoveredMaterial != currentSelectedMaterial) {
				currentHoveredMaterial.emissive.setHex(0x000000);
			}
			currentHoveredMaterial = undefined;
			if (bodyHovered) {
				document.getElementById("bodyDisplayArea").style.cursor = "pointer";
				showBodyTooltip("Body", window_x, window_y);
			} else {
				hideTooltip();
				document.getElementById("bodyDisplayArea").style.cursor = "auto";
			}
			
		}
	};
	
	var removeGeometry = function(systemName, name) {
		if (removeWhenNotVisible) {
			var systemMeta = modelsLoader.getSystemMeta(currentSpecies);
			if (systemMeta[systemName].hasOwnProperty(name) && systemMeta[systemName][name].geometry) {
				currentScene.removeZincGeometry(systemMeta[systemName][name].geometry);
				systemMeta[systemName][name]["loaded"] = PJP.ITEM_LOADED.FALSE;
				systemMeta[systemName][name].geometry = undefined;
			}
			
		}
	}
	
	
	/**
	 * This is called when a body part visibility control is switch on/off.
	 * @callback
	 */
	var changeBodyPartsVisibility = function(name, systemName) {
		return function(value) { 
			var systemMeta = modelsLoader.getSystemMeta(currentSpecies);
			if (systemMeta[systemName].hasOwnProperty(name) && systemMeta[systemName][name].geometry) {
				systemMeta[systemName][name].geometry.setVisibility(value);
			}
			var isPartial = false;
			if (value == false) {
				removeGeometry(systemName, name);
				systemPartsGuiControls[systemName].All = false;
				for (var partName in systemPartsGuiControls[systemName]) {
					if (partName != "All" && systemPartsGuiControls[systemName].hasOwnProperty(partName)) {
						if (systemPartsGuiControls[systemName][partName] == true) {
							isPartial = true;
							break;
						}
					}
				}
				updateSystemButtons(systemName, false, isPartial);
			} else {
				readModel(systemName, name, false);
				for (var partName in systemPartsGuiControls[systemName]) {
					if (partName != "All" && systemPartsGuiControls[systemName].hasOwnProperty(partName)) {
						if (systemPartsGuiControls[systemName][partName] == false) {
							updateSystemButtons(systemName, false, true);
							return;
						}
					}
				}
				systemPartsGuiControls[systemName].All = true;
			}
			updateSystemButtons(systemName, systemPartsGuiControls[systemName].All, isPartial);
			for (var i = 0; i < systemGuiFolder[systemName].__controllers.length; i++) {
				if (systemGuiFolder[systemName].__controllers[i].property == "All") {
					systemGuiFolder[systemName].__controllers[i].updateDisplay();
					systemGuiFolder[systemName].__controllers[i].__prev = 
						systemGuiFolder[systemName].__controllers[i].__checkbox.checked;
					return;
				}
			}
		}
	}
	
	var updateSystemButtons = function(systemName, value, isPartial) {
		var element = document.getElementById(systemName);
		if (value == true)
			element.className = "w3-circle systemToggleButton systemToggleButtonOn";
		else {
			if (isPartial)
				element.className = "w3-circle systemToggleButton systemToggleButtonPartial";
			else
				element.className = "w3-circle systemToggleButton systemToggleButtonOff";
		}
	}
	
	var toggleSystem = function(systemName, value) {
		systemPartsGuiControls[systemName]["All"] = value;
		for (var partName in systemPartsGuiControls[systemName]) {
			if (partName != "All" && systemPartsGuiControls[systemName].hasOwnProperty(partName)) {
				if (systemPartsGuiControls[systemName][partName] != value) {
					var systemMeta = modelsLoader.getSystemMeta(currentSpecies);
					if (systemMeta[systemName].hasOwnProperty(partName)) {
						systemPartsGuiControls[systemName][partName] = value;
						if (systemMeta[systemName][partName].geometry) {
							systemMeta[systemName][partName].geometry.setVisibility(value);
							removeGeometry(systemName, partName);
						}
						if (value == true) {
							readModel(systemName, partName, false);
						}
					}
				}
			}
		}
		for (var i = 0; i < systemGuiFolder[systemName].__controllers.length; i++) {
			systemGuiFolder[systemName].__controllers[i].updateDisplay();
			readModel(systemName, partName, true);
			systemGuiFolder[systemName].__controllers[i].__prev = 
				systemGuiFolder[systemName].__controllers[i].__checkbox.checked;
		}
		updateSystemButtons(systemName, value, false);
	}
	
	var toggleSystemCallback = function(systemName) {
		return function(value) { 
			toggleSystem(systemName, value);
		}
	}
	
	var _addSystemPartGuiControl = function(systemName, partName, item, geometry, visible) {
		if (systemName) {
			if (systemGuiFolder[systemName] !== undefined &&
				systemPartsGuiControls.hasOwnProperty(systemName)) {
				if (!systemGuiFolder[systemName].hasOwnProperty(partName)) {
					systemPartsGuiControls[systemName][partName] = visible;
					systemGuiFolder[systemName].add(systemPartsGuiControls[systemName], partName).onChange(changeBodyPartsVisibility(partName, systemName));
					if (visible == true) {
						for (var partName in systemPartsGuiControls[systemName]) {
							if (partName != "All" && systemPartsGuiControls[systemName].hasOwnProperty(partName)) {
								if (systemPartsGuiControls[systemName][partName] == false) {
									updateSystemButtons(systemName, false, true);
									return;
								}
							}
						}
						systemPartsGuiControls[systemName].All = true;
						updateSystemButtons(systemName, true, false);
						for (var i = 0; i < systemGuiFolder[systemName].__controllers.length; i++) {
							if (systemGuiFolder[systemName].__controllers[i].property == "All") {
								systemGuiFolder[systemName].__controllers[i].updateDisplay();
								systemGuiFolder[systemName].__controllers[i].__prev = 
									systemGuiFolder[systemName].__controllers[i].__checkbox.checked;
								return;
							}
						}
					}
				}
			}
		}
	}
	
	var systemButtonPress = function(receivedElement) {
		var systemName = receivedElement.id;
		toggleSystem(systemName, !(systemPartsGuiControls[systemName]["All"]));
	}
	
	var _transformBodyPart = function(key, geometry) {
		if (modelsTransformationMap.hasOwnProperty(key) &&
				modelsTransformationMap[key].transformation !== undefined)
			geometry.morph.applyMatrix (modelsTransformationMap[key].transformation);
	};
	
	var _addBodyPartCallback = function(systemName, partName, item, scaling, useDefautColour, startup) {
		return function(geometry) {
			//_transformBodyPart(key, geometry);
			item["loaded"] = PJP.ITEM_LOADED.TRUE;
			item.geometry = geometry;
			if (startup)
				_addSystemPartGuiControl(systemName, partName, item, geometry, (item["loaded"] == PJP.ITEM_LOADED.TRUE));
			if (scaling == true) {
				geometry.morph.scale.x = 1.00;
				geometry.morph.scale.y = 1.00;
				geometry.morph.scale.z = 1.03;
				//geometry.morph.position.y = 20;
				geometry.morph.position.z = -47;
			}
			if (useDefautColour)
				modelsLoader.setGeometryColour(geometry, systemName, partName);
			if (partName == "Body") {
				geometry.setAlpha(0.5);
				geometry.morph.material.side = THREE.FrontSide;
			}
			geometry.morph.userData = [systemName, partName];
		}
	};
	
	/** This method add all system folders to the dat.gui user interface.
	 * 
	 */
	var addSystemFolders = function() {
		for (var key in systemGuiFolder) {
			if (systemGuiFolder.hasOwnProperty(key) && systemPartsGuiControls.hasOwnProperty(key)) {
				systemGuiFolder[key] = bodyGui.addFolder(key);
				systemGuiFolder[key].close();
				systemPartsGuiControls[key]["All"] = false;
				systemGuiFolder[key].add(systemPartsGuiControls[key], "All").onChange(toggleSystemCallback(key));
			}
		}
	}
	
	var bodyBackGroundChanged = function() {
		return function(value) {
			var redValue = parseInt(value[0]);
			var greenValue = parseInt(value[1]);
			var blueValue = parseInt(value[2]);
			
			var backgroundColourString = 'rgb(' + redValue + ',' + greenValue + ',' + blueValue + ')';
			var colour = new THREE.Color(backgroundColourString);
			var internalRenderer = bodyRenderer.getThreeJSRenderer();
			internalRenderer.setClearColor( colour, 1 );
		}
	}
	
	/** Initialise everything in the bodyRender, including the 3D renderer,
	 *  dat.gui UI and picker for the 3D renderer.
	 * 
	 */
	var initialiseBodyRenderer = function() {
		bodyRenderer = PJP.setupRenderer("bodyDisplayArea");
		bodyGui = new dat.GUI({autoPlace: false});
		bodyGui.domElement.id = 'gui';
		var control = new bodyControl();
		var controller = bodyGui.addColor(control, 'Background');
		controller.onChange(bodyBackGroundChanged());
		bodyGui.close();
		addSystemFolders();
		var customContainer = document.getElementById("bodyGui").append(bodyGui.domElement);
		var resetViewButton = { 'Reset View':function(){ bodyRenderer.resetView() }};
		var scene = bodyRenderer.createScene("human");
		bodyRenderer.setCurrentScene(scene);
		scene.loadViewURL(modelsLoader.getBodyDirectoryPrefix() + "/body_view.json");
		currentScene = scene;
		var directionalLight = scene.directionalLight;
		directionalLight.intensity = 1.4;
		var zincCameraControl = scene.getZincCameraControls();
		zincCameraControl.enableRaycaster(scene, _pickingBodyCallback(), _hoverBodyCallback());
		zincCameraControl.setMouseButtonAction("AUXILIARY", "ZOOM");
		zincCameraControl.setMouseButtonAction("SECONDARY", "PAN");
	
		bodyGui.add(resetViewButton, 'Reset View');
		bodyRenderer.animate();
	}
	
	var systemButtonPressCallback = function(element) {
		return function() {
			systemButtonPress(element);
		}
	}
	
	var changeSpecies = function(element) {
		currentSpecies = element.value;
		var scene = bodyRenderer.getSceneByName(element.value);
		if (scene == undefined) {
			scene = bodyRenderer.createScene(element.value);
		}
		currentScene = scene;
		bodyRenderer.setCurrentScene(scene);
	}
	
	var addUICallback = function() {
		var callbackContainer = document.getElementById("systemToggle");
		var inputs, index;
		inputs = callbackContainer.getElementsByTagName('input');
		for (var i = 0; i < inputs.length; ++i) {
			inputs[i].onclick = systemButtonPressCallback(inputs[i]); 
		}
		var speciesSelected = document.getElementById("bodySpeciesSelect");
		speciesSelected.onchange = function() { changeSpecies(speciesSelected) };
	}
	
	var loadHTMLComplete = function(link) {
		return function(event) {
			var localDOM = document.getElementById(PanelName);
			var childNodes = null;
			if (link.import.body !== undefined)
				childNodes = link.import.body.childNodes;
			else if (link.childNodes !== undefined)
				childNodes = link.childNodes;
			for (i = 0; i < childNodes.length; i++) {
				localDOM.appendChild(childNodes[i]);
			}	
			addUICallback();
			initialiseBodyRenderer();
			document.head.removeChild(link);
			UIIsReady = true;
		}
	}
		
	/**
	 * Initialise the {@link PJP.BodyViewer}, it will load snippets/bodyViewer.html 
	 * which contains the general layout of this viewer, this is called when 
	 * the {@link PJP.BodyViewer} is created.
	 */
	var initialise = function() {
		var link = document.createElement('link');
		link.rel = 'import';
		link.href = 'snippets/bodyViewer.html';
		link.onload = loadHTMLComplete(link);
		link.onerror = loadHTMLComplete(link);
		document.head.appendChild(link);
	}
	
	var readModel = function(systemName, partName, startup) {
		var systemMeta = modelsLoader.getSystemMeta(currentSpecies);
		item = systemMeta[systemName][partName];
		if (item["loaded"] ==  PJP.ITEM_LOADED.FALSE) {
			var downloadPath = item["BodyURL"];
			var scaling = false;
			item["loaded"] =  PJP.ITEM_LOADED.DOWNLOADING;
			if (item["FileFormat"] == "JSON") {
				if (systemName == "Musculo-skeletal" || systemName == "Skin (integument)")
					scaling = true;
				currentScene.loadMetadataURL(downloadPath, _addBodyPartCallback(systemName, partName, item, scaling, false, startup));
			}
			else if (item["FileFormat"] == "STL")
				currentScene.loadSTL(downloadPath, partName, _addBodyPartCallback(systemName, partName, item, scaling, true, startup));
			else if (item["FileFormat"] == "OBJ") 
				currentScene.loadOBJ(downloadPath, partName, _addBodyPartCallback(systemName, partName, item, scaling, true, startup));
		}
	}
	
	var readBodyRenderModel = function(systemName, partMap) {
		for (var partName in partMap) {
			if (partMap.hasOwnProperty(partName)) {
				var item = partMap[partName]; toggleSystem
				item["loaded"] =  PJP.ITEM_LOADED.FALSE;
				if (item["loadAtStartup"] == true) {
					readModel(systemName, partName, true);
				} else {
					_addSystemPartGuiControl(systemName, partName, item, undefined, false);
				}
			}
		}
	}
	
	/**
	 * Signal the {@link PJP.BodyViewer} to start reading the meta file once the UI is ready.
	 * @async
	 */
	this.readSystemMeta = function() {
		if (UIIsReady) {
			var systemMeta = modelsLoader.getSystemMeta(currentSpecies);
			for (var systemItem  in systemMeta) {
				readBodyRenderModel(systemItem, systemMeta[systemItem]);	
			}
		} else {
			setTimeout(function(){ _this.readSystemMeta(); }, 500);
		}
	}
	
	initialise();
}