Skip to content
Snippets Groups Projects
panther2d.js 33.14 KiB
// jshint esversion: 6 
	let gui = new lil.GUI();
	let hideTimeout = false;
	let svg;
	let zoom = 1;
	let oldZoom = 1;
	let panZoomPanther;
	let foundAnimate = 1;
	let foundScale = 1.05;
	let myPanZoomTimer = null;
	let historytime = + new Date();
	let fel1 = false;
	let fel2 = false;
	const panZoomTime = 500;
	const facilities = [''];
	const names = [];
	const serviceareanames = [];
	const alias = [];
	let maxZoom = 230;
	const status = [];
	const point = {x: 0, y: 0};
	const pa = document.location.search.replace('?','').split('&');
	const parameters = {};
	const state = document.location.search.indexOf('ps')>-1;
	const vlv = document.location.search.indexOf('vlv')>-1;
	const vlvs = [];
	for (let i=0; i<pa.length; i++) {const p = pa[i].split('='); parameters[p[0]] = p[1];}
	const machineCaseSensitive = document.location.search.indexOf('machine=')>-1? document.location.search.split('machine=')[1].split('&')[0]: 'elettra';
	const machine = machineCaseSensitive.toLowerCase();
	//https://puma-01.elettra.eu/rchan.php?json&valueOnly&src=srv-tango-srf-01.fcs.elettra.trieste.it:20000/f/access_control/safety/Undulator_access_state -> 5 FEL1, 6 FEL2
	if (machine.indexOf('fermi')>-1) {
		fetch(conf.rchan+'srv-tango-srf-01:20000/f/access_control/safety/Undulator_access_state').then((response) => {return response.json();}).then((fel) => {
			fel1 = fel[5] == 1;
			fel2 = fel[6] == 1;
		});
	}
	const latticeFile = document.location.href.split('?')[0].split('/').slice(0,-1).join('/')+'/'+machine+'_lattice.json';
	const params = {machine: machineCaseSensitive.toLowerCase(), search: '', backgroundColor: '#333333'};
	gui.title('PAnTHer - controls');
	// if (navigator.userAgent.indexOf('Firefox/63')>-1) {$( "body" ).append('<button id="starter" style="align: center;height: 500px; width: 95%;background-color: #449944; font-size: 100px;" onClick="mystart()">START</button>');}
	function mystart() {
		panZoomPanther = svgPanZoom('#panther', {beforeZoom: myZoom, fit: false, contain: false});
		$("#sname").on("keydown", searchText);
		$('#starter').hide();
	}
	// Polyfill for parentNode.replaceChildren()
	if (typeof Element.prototype.replaceChildren !== 'function') {
		Object.defineProperty(Element.prototype, 'replaceChildren', {
			configurable: true,
			writable: true,
			value: function replaceChildren(...nodes) {
				// Remove all existing child nodes
				while (this.firstChild) {
					this.removeChild(this.firstChild);
				}
				// Append new DOM objects
				this.append(...nodes);
			}
		});
	}
	function toggleMachine(machine) {
		document.location = document.location.href.split('?')[0] + '?machine='+machine;
		/*if (document.location.search.indexOf('machine=')==-1) document.location = document.location.href + (document.location.href.indexOf('?')==-1? '?': '&') + 'machine='+machine;
		let search = document.location.search.split('machine=')[1].split('&')[0];
		document.location = document.location.href.replace('machine='+search, 'machine='+machine);*/
	}
	function compLink(event) {
		console.log('compLink()', event, document.getElementById("compdb").href);
		window.open(document.getElementById("compdb").href, '_blank').focus();
		event.stopPropagation();
		return false;
	}
	function searchText(e) {
		console.log('searchText()', e, e.keyCode || e.which, $("#sname").val().toUpperCase().replace('.','_'));
		if (e.keyCode == 13) findComponent($("#sname").val().toUpperCase().replace('.','_'));
		// return -1;
	}
	// if (navigator.userAgent.indexOf('Firefox/63')==-1) {gui.add(params, 'machine', conf.machineList).onChange(function() {toggleMachine(params.machine);});}
	gui.add(params, 'machine', conf.machineList).onChange(function() {toggleMachine(params.machine);});
	gui.add(params, 'search');
	gui.addColor(params, 'backgroundColor').onChange(function() {toggleParam('backgroundColor');});
	params.vlv = document.location.search.indexOf('vlv')>-1;
	gui.add(params, 'vlv').name('vlv & bst').onChange(function() {toggleParam('vlv');});
	params.ps = document.location.search.indexOf('ps')>-1;
	gui.add(params, 'ps').onChange(function() {toggleParam('ps');});
	const sstring = $('.controller.string').children().eq(1).children().eq(0);
	sstring.attr('id', 'sname');
	sstring.attr('name', 'sname');
	sstring.addClass("form-control sname");
	function findComponent(name) {
		let servicearea = false;
		console.log('lattice', lattice);
		if (lattice.servicearea) for (let i in lattice.servicearea.sections) {
			for (let j in lattice.servicearea.sections[i].components) {
				if (name.replace('.', '_')==lattice.servicearea.sections[i].components[j].name.replace('.', '_')) {servicearea = true;}
				for (let k in lattice.servicearea.sections[i].components[j].embedded) {
					if (name==lattice.servicearea.sections[i].components[j].embedded[k]) {servicearea = true;}
				}
			}
		}
		console.trace(name, servicearea, document.location.search.indexOf('servicearea'), document.location.search.indexOf('search='+name)); // return;
		if ((servicearea != document.location.search.indexOf('servicearea')>-1) && document.location.search.indexOf('search='+name)==-1) {
			document.location = './panther2d.php?machine='+machine+'&search='+name+(servicearea? '&servicearea': '');
		}
		if (name==null) name = document.getElementById('sname').value;
		for (let i=0; i<alias.length; i++) if (alias[i][1]==name) name = alias[i][0];
		document.getElementById('sname').value = name;
		if (typeof $('#'+name)[0] == 'undefined') return;
		console.log(name, window.innerWidth/2, $('#'+name)[0].getCTM().e, $('#'+name)[0].getCTM().f);
		// panZoomPanther.zoomAtPoint(10, {x: window.innerWidth/2 - $('#'+name)[0].getCTM().e, y: window.innerHeight/2 - $('#'+name)[0].getCTM().f})
		panZoomPanther.zoom(10);
		// leave a delay between zoom and pan
		setTimeout(mypan, 1200, name);
	}
	function mypan(name) {
		$('.label').show(); 
		const x = window.innerWidth/2 - $('#'+name)[0].getCTM().e + panZoomPanther.getPan().x;
		const y = window.innerHeight/2 - $('#'+name)[0].getCTM().f + panZoomPanther.getPan().y;
		const m = $("svg")[0].getTransformToElement($('#'+name)[0]);
		panZoomPanther.pan({x: x, y: y});
		setTimeout(pinhide, 100, name,  $('#'+name).eq(0).attr('transform'));
	}
	function pinhide(name, transform) {
		console.log(foundAnimate, foundScale);
		foundAnimate *= foundScale;
		if (foundAnimate>1.5) foundScale = 0.95;
		if (foundAnimate<1) {foundScale = 1.05; foundAnimate = 1;} else setTimeout(pinhide, 100, name, transform);
		$('#'+name).eq(0).attr('transform',transform+',scale('+foundAnimate+')');
	}
	// $(function() {$(".sname").autocomplete({source: names, close: function(event, ui) {findComponent($('#sname').val()); }, select: function(event, ui) {findComponent(ui.item.value); return false;}});});
	$(function() {$(".sname").autocomplete({source: names, select: function(event, ui) {findComponent(ui.item.value); return false;}});});
	function toggleParam(name) {
		if (name=='backgroundColor') {$('body').css('backgroundColor', params.backgroundColor); return;}
		const urlparam = {};
		let search = document.location.search.replace('?', '').split('&');
		if (search[0]=='') search.splice(0,1);
		const i=search.indexOf(name);
		if (i==-1) search.push(name); else search.splice(i,1);
		const res = search.join('&');
		document.location = document.location.href.split('?')[0]+(res.length? '?'+res: '');
	}
	async function subscribe() {
		let response = await fetch("./misc/talk.php?read");
		if (response.status == 502) {
			await subscribe();
		} else if (response.status != 200) {
			// An error - let's show it
			mylog('subscribe() ERROR ', response.statusText);
			// Reconnect in one second
			await new Promise(resolve => setTimeout(resolve, 1000));
			await subscribe();
		} else {
			// Get and show the message
			let message = await response.text();
			mylog('subscribe()', message);
			highlightobjects(message);
			// Call subscribe() again to get the next message
			await subscribe();
		}
	}
	// subscribe();

	function mylog(...args) {
		if (document.location.search.indexOf('debug')>-1) $('#debug').html($('#debug').html()+JSON.stringify(args).replaceAll(':',': ').replaceAll(',',', ')+'\n');
		else {console.trace.apply(null, args);}
	}
	function initIndex(lattice) {
		const index = [];
		for (let l in lattice.conf.index) {
			if (l=='start') continue;
			const cmd = "findComponent('"+lattice.conf.index[l].replace('.','_')+"')";
			index.push('<button onclick="'+cmd+'">'+l+'</button>');
		}
		$('body').append('<div style="position: absolute; left: 5px; bottom: 5px;">'+index.join(' ')+'</div>');
	}
	function init() {
		fetch(latticeFile).then((response) => {return response.json();}).then((flattice) => {
			lattice = flattice;
			if (Object.keys(lattice).length>0) {
				for (let i in lattice) {if (i!='conf') facilities.push(i);}
				for (let i in lattice) {
					if (i == 'conf') continue;
					// logic XOR https://stackoverflow.com/questions/2335979/is-there-anyway-to-implement-xor-in-javascript
					if ((document.location.search.indexOf('servicearea')==-1) != (i=='servicearea')) initLattice(lattice[i].sections, i); else initSearch(lattice[i].sections, i);
				}
				bpmInit(facilities); 
				if (document.location.search.indexOf('servicearea')>-1) initSearch(lattice.servicearea.sections, 'servicearea');
				if (typeof blm != 'undefined') blmMenu(lattice, facilities, params);
				if (typeof bpmData != 'undefined') bpmMenu(lattice, facilities, params);
				params.gotoAdmin = function() {document.location = './admin.php';};
				gui.add(params, 'gotoAdmin').name('Admin');
				params.goto3D =  function() {document.location = './panther.php?machine='+params.machine;}; gui.add(params, 'goto3D').name('3D');
				if (lattice.conf && lattice.conf.index) initIndex(lattice);
			}
			$('.scale').attr('transform', "scale(2)");
			// {$('<div><iframe style="width: 100%;height:250px;" src="../misc/gauge.html?dark&r1only=1&r=100&max=360&throttlingPeriod=50&apply=rotate&val='+0+'"></iframe></div>').insertBefore('.function');}
			// panZoomPanther = svgPanZoom('#panther', {beforeZoom: myZoom, fit: false, contain: false}); // https://github.com/bastienmoulia/svg-pan-zoom-rotate
			// if( /Android|webOS|iPhone|iPad|Mac|Macintosh|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent) ) {
			if( /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent) ) {
				panZoomPanther = svgPanZoom('#panther', {
					zoomEnabled: true,
					controlIconsEnabled: false,
					fit: 0,
					center: 1,
					maxZoom: maxZoom*2,
					customEventsHandler: eventsHandler,
					//beforeZoom: myZoom,
				});
			}
			else {
				panZoomPanther = svgPanZoom('#panther', {
					zoomEnabled: true,
					zoomScaleSensitivity: 0.3,
					controlIconsEnabled: false,
					fit: false,
					center: false,
					minZoom: 0.2,
					maxZoom: maxZoom,
					beforePan: myPan,
					beforeZoom: myZoom,
				});
			}
			if (document.location.search.indexOf('debug')>-1) {
				$('#tooltip').show();
				$('#tooltip').css('width', '100%');
				$('#tooltip').html('<textarea id="debug" style="height: 100px; width:100%;"></textarea>');
			}
			let pan = [window.innerWidth/2, window.innerHeight/2];
			if (document.location.search.indexOf('pan=')>-1) pan = document.location.search.split('pan=')[1].split('&')[0].split(',');
			if (document.location.search.indexOf('search=')>-1) findComponent(document.location.search.split('search=')[1].split('&')[0]);
			else {
				const zoom = document.location.search.indexOf('zoom=')>-1? document.location.search.split('zoom=')[1].split('&')[0]-0: 0.5;
				panZoomPanther.zoom(zoom);
				setTimeout(panZoomPanther.pan, 600, {x: pan[0], y: pan[1]});
			}
			if (state) {
				fetch(conf.stateSrcUrl, {cache: "no-store"}).then((response) => {return response.text();}).then((data) => {
					const statSrc = data.toUpperCase().split('.').join('_').split(',');
					for (let j=0; j<statSrc.length; j++) {
						// if (statSrc[j].split('/')[2]=='POWER_SUPPLY' || statSrc[j].split('/')[2]=='INJECTION' || statSrc[j].split('/')[2]=='EXTRACTION') 
							statSrc[j] = statSrc[j].split('/')[3].replace('PS', '');
					}
					for (let i=0; i<status.length; i++) { 
							if (status[i].name.indexOf('KICK')>-1) console.log('KICK state', i, status[i].name, statSrc);
						for (let j=0; j<statSrc.length; j++) {
							if (status[i].name.indexOf(statSrc[j])>-1) {status[i].statsrc = state; status[i].statindex = j;}
						}
					}
					// console.log('statSrc', data, statSrc, status);
				});
				setInterval(updateStatus, 1000);
			}
			if (vlv) {
				for (let i in conf.bstmap) { if (i.indexOf('.')>-1) conf.bstmap[i.replace('.','_')] = conf.bstmap[i];}
				fetch(conf.vlvSrcUrl, {cache: "no-store"}).then((response) => {return response.text();}).then((data) => {
					const vlvSrc = data.toUpperCase().substring(14).split(',');
					for (let i=0; i<vlvs.length; i++) { 
						for (let j=0; j<vlvSrc.length; j++) {
							const vlva = vlvSrc[j].split('/');
							if (vlva.length < 4) continue;
							const name = vlva[3].replace('.','_');
							if (vlvs[i].name.replace('.','_').indexOf(name)>-1) {vlvs[i].vlvsrc = state; vlvs[i].vlvindex = j; vlvs[i].type = 'vlv';}
							if (vlvSrc[j].split('/')[2].indexOf(conf.bstmap.base)>-1) {
								if (conf.bstmap[vlvs[i].name] && conf.bstmap[vlvs[i].name].indexOf(vlvSrc[j].split('/')[4])>-1) {
									vlvs[i].vlvsrc = 'bst'; vlvs[i].vlvindex = j; vlvs[i].type = 'bst';
								}
							}
						}
					}
					//console.log('vlvSrc', data, vlvSrc, vlvs);
				});
				setInterval(updateVlv, 1000);
			}
			$("#sname").on("keydown", searchText);
		});
	}
	function showStatus(i, stat) {
		if (stat == 0 || stat == 'null' || stat == '' || stat == 'ON' || stat == 'RUNNING' || (!fel1 && $('#'+status[i].name)[0].classList[0]=='fel1') || (!fel2 && $('#'+status[i].name)[0].classList[0]=='fel2')) {$('#'+status[i].name).hide();}
		else {$('#'+status[i].name).show(); document.getElementById(status[i].name).style.fill = conf.stateLabelColor[stat];}
		// console.log(i, status[i], stat);
	}
	function clearStatus() {
		$('.ps').hide();
	}
	function updateStatus() {
		fetch(conf.stateUrl, {cache: "no-store"}).then((response) => {return response.text();}).then((data) => {
			const statVal = data.split(';');
			// console.log(conf.stateUrl, statVal, status);
			for (let i=0; i<status.length; i++) {
				if (status[i].statsrc==state) {
					if (status[i].statindex) showStatus(i, statVal[status[i].statindex]);
					// console.log(statVal, status, status[i].name, i, status[i].statindex, statVal[status[i].statindex]);
				}
			}
			setTimeout(clearStatus, 600);
		});
	}
	function updateVlv() {
		fetch(conf.vlvUrl, {cache: "no-store"}).then((response) => {return response.text();}).then((data) => {
			const vlvVal = data.split(':')[1].split(';');
			// console.log('updateVlv()', conf.vlvUrl, vlvVal, vlvs);
			for (let i=0; i<vlvs.length; i++) {
				if (vlvs[i].type=='vlv') $('#'+vlvs[i].name).css('fill', vlvVal[vlvs[i].vlvindex]=='CLOSED'? 'yellow': (vlvVal[vlvs[i].vlvindex]=='OPENED'? 'limegreen': 'grey'));
				if (vlvs[i].type=='bst') {
					if (typeof vlvVal[vlvs[i].vlvindex] != 'string') continue;
					const val = vlvVal[vlvs[i].vlvindex].split(',');
					// console.log('updateVlv(): ', i, vlvs[i], vlvs[i].type, vlvs[i].vlvindex, vlvVal[vlvs[i].vlvindex], '#'+vlvs[i].name, vlvs[i].comp, val[0]=='true'? 'limegreen': (val[1]=='true'? 'yellow': 'grey'));
					$('#'+vlvs[i].name).css('fill', val[0]=='true'? 'limegreen': (val[1]=='true'? 'yellow': 'grey'));
				}
			}
		});
	}
	function rescale(x) {return x;}
	String.prototype.replaceAt = function(index, replacement) {
		return this.substring(0, index) + replacement + this.substring(index + replacement.length);
	};
	function evalId(base) {
		if (base.indexOf('.')>-1) return base;
		return base.replaceAt(base.lastIndexOf('_'), '.').replace('RTBPM','BPM');
	}
	function openTooltip(event) {
		if (document.location.search.indexOf('debug')==-1) {
			const type = this.href? this.href.baseVal.replace('#',''): (this.id.indexOf('BLM')>-1 || this.id.indexOf('BERGOZ')>-1? 'blm': '???');
			mylog('openTooltip()',type, this.id, event, event.clientY);
			$('#tooltip').css('left', event.clientX+30);
			$('#tooltip').css('top', event.clientY+30);
			document.getElementById('tooltipFrame').src = conf.tooltipApp+'?s='+type.replace('fast', '')+'&param='+this.id;
			console.log('openTooltip(event)', type.toLowerCase(), type.toLowerCase().indexOf('beamline'));
			document.getElementById('tooltip').style.display = 'block';
			if (hideTimeout!==false) clearTimeout(hideTimeout);
			hideTimeout = setTimeout(hideTooltip, 120000);
			document.getElementById('compdb').style.display = (type.indexOf('rv')==0 || type.indexOf('rc')==0 || type.indexOf('rid')==0 || type.indexOf('rd')==0 || type.indexOf('rps')==0 || type.indexOf('plc')==0)? 'none': 'block';
			const id = evalId(this.id);
			if (document.getElementById('compname')) document.getElementById('compname').innerHTML = id;
			document.getElementById('compdb').href = conf.compdb + id;
			document.getElementById("compdb").addEventListener("click", compLink);
			if (type.toLowerCase().indexOf('beamline')>-1) {
				document.getElementById('compdb').href = 'http://adam.elettra.trieste.it/projects/blcs/beamwatch/';
				document.getElementById('compdb').innerHTML = 'search '+this.id+' in ADAM Beamwatch <span id="compname"/>';
			}
			else if (machine=='elettra') document.getElementById('compdb').setAttribute("disabled", true);
			event.stopPropagation();
		}
		else {
			for (let l in lattice) {
				if (l=='confg') continue;
				let servicearea = false;
				for (let i in lattice[l].sections) {
					for (let j in lattice[l].sections[i].components) {
						if (this.id==lattice[l].sections[i].components[j].name.replace('.', '_')) {mylog('openTooltip()', lattice[l].sections[i].components[j], lattice[l].sections[i].components[j].position-document.location.search.split('offset=')[1].split('&')[0]);}
					}
				}
			}
		}
	}
	function hideTooltip() {
		if (hideTimeout!==false) clearTimeout(hideTimeout);
		hideTimeout = false;
		// mylog('hideTooltip');
		if (document.location.search.indexOf('debug')==-1) {
			document.getElementById('tooltipFrame').src = '';
			document.getElementById('tooltip').style.display = 'none';
		}
	}
	function transformLabel(x, y, beta, labelReverse) {
		if (typeof labelReverse == 'object') return "translate("+rescale(x)+" "+rescale(y)+") rotate("+(beta+labelReverse[0])+") translate("+labelReverse[1]+" "+labelReverse[2]+")";
		return labelReverse==180? "translate("+rescale(x)+" "+rescale(y)+") rotate("+(beta-90)+") translate(1800 100)":
				"translate("+rescale(x)+" "+rescale(y)+") rotate("+(beta+90)+") translate("+(labelReverse? -250: 250)+" "+(labelReverse? 200: 300)+")";
	}
	function appendLabel(id, labelclass, display, x, y, beta, labelReverse) {
		if ((beta+3600)%360 <180 && typeof labelReverse != 'object') labelReverse = [-90, -250, -100];
		appendSvg("text", {
			id: id+'label', 
			class: labelclass, 
			x:0, y:0, style:"display: "+display, fill:"white", stroke:"#eeeeee","stroke-width":10, "font-family":"Arial", "font-size":200, "font-weight":"bold", 
			"text-anchor": (labelReverse? "end": "start"),
			transform: transformLabel(x, y, beta, labelReverse)
		}, false, id);
	}
	function appendLabel2(param, labelclass, display, x, y, beta, labelReverse) {
		const id = param.name;
		const fontsize = labelclass.indexOf('bl')>-1? 800: 500;
		// console.log("appendLabel2()",param, labelclass, display, x, y, beta, labelReverse);
		if (labelclass.indexOf('bl')>-1 && typeof labelReverse != 'object') {
			labelReverse = [180, 17000, param.type=='beamlineUp'? -500: +800];
			if ((beta+3690)%360 < 180) labelReverse = [0, -14000, param.type=='beamlineUp'? 1000: -300];
		}
		else if ((beta+3600)%360 < 180 && typeof labelReverse != 'object') labelReverse = [-90, -250, -100];
		appendSvg("text", {
			id: id+'label', 
			class: labelclass, 
			x:0, y:0, style:"display: block", fill:"red", stroke:"pink","stroke-width":10, "font-family":"Arial", "font-size":fontsize, "font-weight":"bold", 
			"text-anchor": (labelReverse? "end": "start"),
			transform: transformLabel(x, y, beta, labelReverse)
		}, false, id);
	}
	function appendSvg(tagName, attrib, onClickCall=false, text=false, myclass=false) {
		const elem = document.createElementNS("http://www.w3.org/2000/svg", tagName);
		if (onClickCall) elem.addEventListener("click", onClickCall, false);
		if (text) {				
			const textNode = document.createTextNode(text);
			elem.appendChild(textNode);
		}
		const jelem = $(elem);
		if (myclass) {/*mylog(elem, jelem, myclass);*/ elem.classList.add(myclass);}
		for (let i in attrib) {
			jelem.attr(i, attrib[i]);
		}
		$("svg").append(jelem);
	}
	function appendSearch(component, facility) {
		// mylog('appendSearch()',component, facility);
		if (component) {
			const comp = component.type.replace('booster', '');
			if (comp && comp!=' ' && $('#'+comp)[0]) {
				const id = extractId(component.name);
				names.push(id[0]);
				if (facility=='servicearea') serviceareanames.push(id[0]);
				if (component.embedded) {
					for (let j=0; j<component.embedded.length; j++)  {names.push(component.embedded[j]); alias.push([id[0],component.embedded[j]]);}
				}
			}
		}
	}
	function appendComponent(components, x0, y0, x1, y1, facility) {
		const dx = x1 - x0;
		const dy = y1 - y0;
		const d = Math.sqrt(dx*dx + dy*dy);
		const beta = 180*Math.atan2(y0-y1, x0-x1)/Math.PI;
		if (components) for (let i=0; i<components.length; i++) {
			const comp = components[i].type.replace('booster', '');
			let x = x0+components[i].position/d*dx;
			let y = y0+components[i].position/d*dy;
			if (components[i].offset2d) {x += components[i].offset2d[0]; y += components[i].offset2d[1];}
			if (typeof blm != 'undefined' && comp=='blm') {
				const name = components[i].name.replace('BPM','BLM');
				// mylog('appendComponent(), ', name, blm.obj);
				blm.obj.push(name);
				blm.dir.push(beta);
				appendSvg("rect", {id:name, name:name, x:0, y:0, width:40, height:40, rx:20, ry:20, transform:"translate("+rescale(x)+" "+rescale(y)+") rotate("+(name.indexOf('_L')>-1? beta + 180: beta)+")"}, openTooltip, false, "blm");
			}
			if (typeof bpmData != 'undefined' && comp=='bpm') {
				// mylog('appendComponent()',components[i], x0, y0, x1, y1, beta, facility, 180*Math.atan2(y0-y1, x0-x1)/Math.PI);
				bpmData[facility].obj.push(components[i].name);
				bpmData[facility].dir.push(beta);
				bpmData[facility].pos.push([rescale(x), rescale(y)]);
			}
			if (typeof bpmData != 'undefined' && comp=='corrector') {
				corr[facility].obj.push(components[i].name);
				corr[facility].dir.push(beta);
				corr[facility].pos.push([x, y]);
			}
			if ($('#'+comp)[0]) {
				// mylog('components['+i+']',components[i]);
				const id = extractId(components[i].name);
				const section = components[i].name.indexOf('_')>-1? components[i].name.split('_')[1].split('.')[0]+' ': '';
				const offset = components[i].type == 'beamlineUp'? "-2100 -3500": "0 -200";
				const transform = "translate("+rescale(x)+" "+rescale(y)+") rotate("+(beta+180)+") translate("+offset+")";
				appendSvg("use", {href:"#"+comp, id: id[0], name:components[i].name, class: comp+' '+section+facility, style:"cursor: pointer", transform:transform}, openTooltip);
				if (components[i].type == 'label' || components[i].type == 'beamlineDown' || components[i].type == 'beamlineUp') 
					appendLabel2(components[i], section+facility, 'none', x, y, beta, components[i].labelReverse);
				else
					appendLabel(components[i].name, comp+' label '+section+facility, 'none', x, y, beta, components[i].labelReverse);
				names.push(id[0]);
				if (components[i].embedded) {
					for (let j=0; j<components[i].embedded.length; j++)  {names.push(components[i].embedded[j]); alias.push([id[0],components[i].embedded[j]]);}
				}
				if (id[1]) {names.push(id[1]); alias.push(id);}
				if (state) {
					if (components[i].ps) {
						for (let pi=0; pi<components[i].ps.length; pi++) {
							const name = components[i].ps[pi].replace('PS','').replace('.','_') + '_status';
							status.push({name: name});
							appendSvg('circle', {id: name, class: facility+' ps', style:"display: none", cx: 150+pi*100, cy: 200, transform:transform, r: 80, stroke: 10, strokeColor: 'blue', fill: 'gray'}, false, false, 'status');
						}
					}
					else {
						status.push({name: id[0]+'_status'});
						appendSvg('circle', {id: id[0]+'_status', class: facility+' ps', style:"display: none", cx: 200, cy: 200, transform:transform, r: 80, stroke: 10, strokeColor: 'blue', fill: 'gray'}, false, false, 'status');
					}
					if (components[i].name.toUpperCase().indexOf('KICK')>-1) console.log('PS: ', components[i].name, components[i].name.toUpperCase().indexOf('KICK'), status);
				}
				if (vlv && (components[i].type == 'vlv' || components[i].type == 'bst')) { 
					// appendSvg('rect', {id: id[0]+'_disable', style:"display: none", x: x+75, y: y-90, width: 50, height: 200, stroke: 10, fill: 'black'}, false, false, 'vlvs');
					vlvs.push({name: id[0], type: components[i].type});
				}
			}
		}
	}
	if (!String.prototype.replaceAll) {
		String.prototype.replaceAll = function(search, replace) {
			return this.split(search).join(replace);
		};
	}
	function extractId(name) {
		if (name.indexOf('(')>-1) {
			let tok = name.split('(');
			return [tok[0].replaceAll('.','_').replaceAll(' ','_').replace(/_+$/, ''), tok[1].replaceAll('.','_').replaceAll(' ','_').replaceAll(')','').replace(/_+$/, '')];
		}
		// .replace(/_+$/, '') => https://stackoverflow.com/questions/8141718/how-to-trim-specific-characters-from-the-end-of-the-javascript-string
		return [name.replaceAll('.','_').replaceAll(' ','_').replace(/_+$/, ''), false];
	}
	function highlightobjects(objclass) {
		$("use").css('opacity',0.3);
		$("text").css('opacity',0.3);
		$("."+objclass).css('opacity',1);
	}
	function magnifyobjects(objclass) {
		$("text").css('opacity',0.15);
		$("text."+objclass).css('opacity',1);
		$('.scale').attr('transform', "scale(1) translate(90,100)");
		let x = -40;
		let y = -40;
		let s = 3;
		if (objclass=="vlv") {x = -50; y = -50; s = 5;}
		$('.'+objclass+'scale').attr('transform', "scale("+s+") translate("+x+","+y+")");
	}
	function initSearch(sections, facility) { 
		console.log('initSearch()', sections, facility);
		for (i=0; i<sections.length; i++) { // if(i>1) break;
			if (sections[i].bending && sections[i].bending.type) {
				console.log('names.push()', sections[i].bending.name);
				names.push(sections[i].bending.name.replace('.','_'));
			}
			if (typeof sections[i].components == 'object') for (let j=0; j<sections[i].components.length; j++) appendSearch(sections[i].components[j], facility);
		}
	}
	function initLattice(sections, facility) { 
		initSearch(sections, facility);
		if (document.location.search.indexOf('facility=')>-1 && facility!=document.location.search.split('facility=')[1].split('&')[0]) return;
		if (typeof bpmData != 'undefined') {
			bpmData[facility] = {obj: [], dir: [], pos: []};
			corr[facility] = {obj: [], dir: [], pos: []};
		}
		// vacuum chamber
		let d = '';
		let m = true;
		let wall = false;
		for (let i=0; i<sections.length; i++) {
			d = d + (m? 'M ': ' L ')+rescale(sections[i].start.x)+' '+rescale(sections[i].start.z);
			if (i<sections.length-1 && typeof sections[i+1].chamber == 'undefined') {m = true;} else if (i<sections.length-1 && sections[i+1].chamber.type=="chamber") {m = false;}
			if (i<sections.length-1 && typeof sections[i+1].chamber != 'undefined' && sections[i+1].chamber.type=="wall") wall = true;
		}
		if (sections[0].chamber && sections[0].chamber.type=='chamber') d = d + ' Z';
		console.log('sections', sections);
		if (d!='') {
			if (wall) appendSvg("path", {d: d, fill: "none", stroke: "#990000", "stroke-dasharray": "200 200", "stroke-width": 50, id:"wall"+facility});
			else appendSvg("path", {d: d, fill: "none", stroke: "#999999", "stroke-width": 20, id:"chamber"+facility});
		}
		let j = 0;
		let i = sections.length - 1;
		let k = sections.length - 2;
		let alpha = 180/Math.PI*Math.atan2(sections[j].start.z-sections[i].start.z, sections[j].start.x-sections[i].start.x);
		for (i=0; i<sections.length; i++) { // if(i>1) break;
			j = (i + 1) % sections.length;
			k = (i + sections.length - 1) % sections.length;
			let beta = 180/Math.PI*Math.atan2(sections[j].start.z-sections[i].start.z, sections[j].start.x-sections[i].start.x);
			let gamma = (beta+alpha)/2;
			if (sections[k] && sections[j].start.z<sections[i].start.z && sections[i].start.z>sections[k].start.z) gamma = gamma + 180;
			if (alpha>beta && gamma<0) gamma = gamma + 180;
			if (sections[i].bending && sections[i].bending.type) {
				const section = sections[i].bending.name.indexOf('_')>-1? sections[i].bending.name.split('_')[1].split('.')[0]+' ': '';
				appendSvg("use", {href:"#"+sections[i].bending.type.replace('dipolefermi','bending'), id:sections[i].bending.name.replace('.','_'), class: 'bending '+section+facility, style:"cursor: pointer", transform:"translate("+rescale(sections[i].start.x)+" "+rescale(sections[i].start.z)+") rotate("+gamma+") translate(-600 -200)"}, openTooltip);
				appendLabel(sections[i].bending.name, 'bending '+section+facility, 'block', sections[i].start.x, sections[i].start.z, gamma+180, sections[i].bending.labelReverse);
				// appendSvg("text", {id:sections[i].bending.name+'label', class: 'bending '+section+facility, x:0, y:0, fill:"white", stroke:"#888888","stroke-width":10, "font-family":"Arial", "font-size":300, "font-weight":"bold", transform:"translate("+rescale(sections[i].start.x)+" "+rescale(sections[i].start.z)+") rotate("+gamma+") translate(-600 -250)"}, false, sections[i].bending.name);
			}
			alpha = beta;
			appendComponent(sections[i].components, sections[i].start.x, sections[i].start.z, sections[j].start.x, sections[j].start.z, facility);
			// if (typeof bpmData != 'undefined') bpmInit(facility); 
		}
	}
	$(document).ready(function() {
		$("svg").attr('height', window.innerHeight+'px');
		init();
	});

	var eventsHandler;
	let myhammer;
	eventsHandler = {
		haltEventListeners: ['touchstart', 'touchend', 'touchmove', 'touchleave', 'touchcancel','tap', 'pinch','pinchstart','pinchmove'], 
			init: function(options) {
			var instance = options.instance, initialScale = 1, pannedX = 0, pannedY = 0;
			// mylog('init', options, panZoomPanther);
			myhammer = Hammer(options.svgElement, {
				inputClass: Hammer.SUPPORT_POINTER_EVENTS ? Hammer.PointerEventInput : Hammer.TouchInput
			});
			myhammer.get('pinch').set({enable: true});
			myhammer.on('panstart panmove', function(ev){
				if (ev.type === 'panstart') {
					pannedX = 0;
					pannedY = 0;
				}
				panZoomPanther.panBy({x: ev.deltaX - pannedX, y: ev.deltaY - pannedY});
				pannedX = ev.deltaX;
				pannedY = ev.deltaY;
			});
			myhammer.on('pinchstart pinchmove', function(ev){
				if (ev.type === 'pinchstart') {
					initialScale = panZoomPanther.getZoom();
					panZoomPanther.zoomAtPoint(initialScale * ev.scale, {x: ev.center.x, y: ev.center.y}, zoom, ev.scale);
					// mylog(''); mylog('panZoomPanther.zoomAtPoint('+(initialScale * ev.scale)+', {x: '+ev.center.x+', y: '+ev.center.y+'})');
				}
				if (ev.type === 'pinchmove') {
					oldZoom = zoom;
					zoom = initialScale * ev.scale;
					// mylog('zoom', zoom, ev.center.x);
					point.x = ev.center.x;
					visibleX = 0 - point.x/zoom;
					visibleWidth = document.getElementById('panther').clientWidth/zoom;
				}
				panZoomPanther.zoomAtPoint(initialScale * ev.scale, {x: ev.center.x, y: ev.center.y});
				zoom = panZoomPanther.getZoom();
				if (zoom>2) $('.label').show(); else $('.label').hide();
				// mylog('panZoomPanther.zoomAtPoint('+(initialScale * ev.scale)+', {x: '+ev.center.x+', y: '+ev.center.y+'})', zoom, ev.scale);
			});
			options.svgElement.addEventListener('touchmove', function(e){e.preventDefault(); });
		}, 
		destroy: function(){
			myhammer.destroy();
		}
	};
	function myPanZoomDelayed(event) {
		if (event) event.preventDefault();
		visibleX = 0 - point.x/zoom;
		visibleWidth = document.getElementById('panther').clientWidth/zoom;
		// mylog('myPanZoomDelayed, event', event, 'point', point, 'zoom:', zoom, 'visibleX', visibleX, 'visibleWidth', visibleWidth);
		myPanZoomTimer = null;
		oldZoom = zoom;
	}
	let firstUrl = true;
	function setUrl(name, value) {
		let t = + new Date();
		if (t - historytime < 100) return;
		historytime = t;
		parameters[name] = value;
		const pp = [];
		for (let i in parameters) {pp.push(i+'='+parameters[i]);}
		const url = document.location.origin+document.location.pathname+'?'+pp.join('&').replaceAll('=undefined', '').replace('search=','s=');
		// console.log('setUrl()',name, value, url);
		if (firstUrl) window.history.pushState({"html":'panther2d.php',"pageTitle":'PAnTHer'},"", url);
		else window.history.replaceState({"html":'panther2d.php',"pageTitle":'PAnTHer'},"", url);
		firstUrl = false;
	}
	function myPan(oldPan, newPan) {
		setUrl('pan', Math.round(newPan.x*100)/100+','+Math.round(newPan.y*100)/100);
		point.x = newPan.x;
		point.y = newPan.y;
		if (myPanZoomTimer == null && document.location.search.indexOf('&autoPanZoom')==-1) myPanZoomTimer = setTimeout(myPanZoomDelayed, panZoomTime);
		// mylog('myPan', p,q);
	}
	function myZoom(oldScale, newScale) {
		setUrl('zoom', Math.round(newScale*100)/100);
		zoom = newScale;
		if (newScale>2) $('.label').show(); else $('.label').hide();
		if (myPanZoomTimer == null && document.location.search.indexOf('&autoPanZoom')==-1) myPanZoomTimer = setTimeout(myPanZoomDelayed, panZoomTime);
		// mylog('myZoom', zoom);
	}
/*
https://github.com/dagrejs/dagre-d3/issues/202
*/
SVGElement.prototype.getTransformToElement = SVGElement.prototype.getTransformToElement || function(elem) {
    return elem.getScreenCTM().inverse().multiply(this.getScreenCTM());
};