var panopticon; class Panopticon { constructor() { console.log( "Init panopticon" ); this.hugveys = new Vue( { el: "#status", data: { uptime: 0, languages: [], hugveys: [] }, methods: { time_passed: function (hugvey, property) { console.log("property!", Date(hugvey[property] * 1000)); return moment(Date(hugvey[property] * 1000)).fromNow(); }, loadNarrative: function(code, file) { return panopticon.loadNarrative(code, file); } } } ); this.socket = new ReconnectingWebSocket( "ws://localhost:8888/ws", null, { debug: true, reconnectInterval: 3000 } ); this.graph = new Graph(); this.socket.addEventListener( 'open', ( e ) => { this.send( { action: 'init' } ); } ); this.socket.addEventListener( 'close', function( e ) { console.log( 'Closed connection' ); } ); this.socket.addEventListener( 'message', ( e ) => { let msg = JSON.parse( e.data ); if ( typeof msg['alert'] !== 'undefined' ) { alert(msg['alert']); } if ( typeof msg['action'] === 'undefined' ) { console.error( "not a valid message: " + e.data ); return; } switch ( msg['action'] ) { case 'status': this.hugveys.uptime = this.stringToHHMMSS(msg['uptime']); this.hugveys.languages = msg['languages']; this.hugveys.hugveys = msg['hugveys']; break; } } ); } send( msg ) { if(this.socket.readyState == WebSocket.OPEN) { this.socket.send( JSON.stringify( msg ) ); } else { console.error("Socket not open: ", this.socket.readyState); } } getStatus() { // console.log('get status', this, panopticon); panopticon.send( { action: 'get_status' } ); } init() { setInterval( this.getStatus, 3000 ); } stringToHHMMSS (string) { var sec_num = parseInt(string, 10); // don't forget the second param var hours = Math.floor(sec_num / 3600); var minutes = Math.floor((sec_num - (hours * 3600)) / 60); var seconds = sec_num - (hours * 3600) - (minutes * 60); if (hours < 10) {hours = "0"+hours;} if (minutes < 10) {minutes = "0"+minutes;} if (seconds < 10) {seconds = "0"+seconds;} return hours+':'+minutes+':'+seconds; } loadNarrative(code, file) { let req = new XMLHttpRequest(); let graph = this.graph; req.addEventListener("load", function(e){ console.log('TEST',this); graph.loadData(JSON.parse(this.response)); // console.log(, e); }); req.open("GET", "/local/" + file); req.send(); } resume(hv_id) { this.send({ action: 'resume', hugvey: hv_id }) } pause(hv_id) { this.send({ action: 'play', hugvey: hv_id }) } restart(hv_id) { this.send({ action: 'restart', hugvey: hv_id }) } } window.addEventListener( 'load', function() { panopticon = new Panopticon(); panopticon.init(); }); class Graph{ constructor() { this.width = 1280; this.height = 1024; this.nodeSize = 80; this.maxChars = 16; this.svg = d3.select('#graph'); this.container = d3.select('#container'); this.selectedMsg = null; this.messages = []; // initialise empty array. For the simulation, make sure we keep the same array object this.directions = []; // initialise empty array. For the simulation, make sure we keep the same array object this.conditions = []; // initialise empty array. For the simulation, make sure we keep the same array object this.interruptions = []; // initialise empty array. For the simulation, make sure we keep the same array object let graph = this; this.controlDown = false; document.addEventListener('keydown', function(e){ console.log(e); if(e.which == "17") { graph.controlDown = true; document.body.classList.add('controlDown'); } }); document.addEventListener('keyup', function(e){ console.log(e); if(e.which == "17") { graph.controlDown = false; document.body.classList.remove('controlDown'); } }); let c = this.container; let zoomed = function(){ c.attr("transform", d3.event.transform); } this.svg.call(d3.zoom() .scaleExtent([1 / 2, 8]) .on("zoom", zoomed)); this.nodesG = this.container.append("g") .attr("id", "nodes") this.linkG = this.container.append("g") .attr("id", "links"); document.getElementById('btn-save').addEventListener('click', function(e){ graph.saveJson(); }); document.getElementById('btn-addMsg').addEventListener('click', function(e){ graph.createMsg(); }); } clickMsg(msg) { // event when a message is clicked. console.log(msg); if(this.controlDown) { this.secondarySelectMsg(msg); } else { this.selectMsg(msg); } } secondarySelectMsg(msg) { if(this.selectedMsg !== null) { this.addDirection(this.selectedMsg, msg); } else { console.error('No message selected as Source'); } } selectMsg(msg) { let selectedEls = document.getElementsByClassName('selectedMsg'); while(selectedEls.length > 0){ selectedEls[0].classList.remove('selectedMsg'); } document.getElementById(msg['@id']).classList.add('selectedMsg'); this.selectedMsg = msg; this.showMsg(msg); } updateMsg() { // used eg. after a condition creation. this.showMsg(this.selectedMsg); } showMsg(msg) { let msgEl = document.getElementById('msg'); msgEl.innerHTML = ""; let startAttributes = { 'name': msg['@id'] + '-start', 'disabled': true, 'type': 'checkbox', 'on': { 'change': this.getEditEventListener() } } if(msg['start'] == true) { startAttributes['checked'] = 'checked'; } let msgInfoEl = crel('div', {'class': 'msg__info'}, crel('h1', {'class':'msg__id'}, msg['@id']), crel('label', crel('span', 'Text'), crel('input', { 'name': msg['@id'] + '-text', 'value': msg['text'], 'on': { 'change': this.getEditEventListener() } } ) ), crel('label', crel('span', 'Start'), crel('input', startAttributes) ) ); msgEl.appendChild(msgInfoEl); // let directionHEl = document.createElement('h2'); // directionHEl.innerHTML = "Directions"; let fromDirections =[] , toDirections = []; for(let direction of this.getDirectionsTo(msg)) { toDirections.push(this.getDirectionEl(direction, msg)); } for(let direction of this.getDirectionsFrom(msg)) { fromDirections.push(this.getDirectionEl(direction, msg)); } let directionsEl = crel('div', {'class': 'directions'}, crel('h2', 'Directions'), ...toDirections, ...fromDirections ); msgEl.appendChild(directionsEl); } getDirectionEl(direction, msg) { let directionEl = document.createElement('div'); if(direction['source'] == msg) { directionEl.innerHTML = `

To ${direction['target']['@id']}

`; } else { directionEl.innerHTML = `

From ${direction['source']['@id']}

`; } let del = document.createElement('div'); del.innerHTML = "delete"; del.classList.add("deleteBtn"); let g = this; del.addEventListener('click', (e) => g.rmDirection(direction)); directionEl.appendChild(del); // TODO; conditions for(let conditionId of direction['conditions']) { let condition = this.getNodeById(conditionId); directionEl.appendChild(this.getEditConditionFormEl(condition, direction)); } directionEl.appendChild(this.getAddConditionFormEl(direction)); return directionEl; } getEditConditionFormEl(condition, direction) { let conditionEl = crel('div', {'class': 'condition condition--edit'}, crel('h4', {'title': condition['@id']}, condition['type']) ) let labelLabel = document.createElement('label'); labelLabel.innerHTML = "Description"; let labelInput = crel('input',{ 'name': `${condition['@id']}-label`, 'value': typeof condition['label'] == 'undefined' ? "" : condition['label'], 'on': { 'change': this.getEditEventListener() } }); labelLabel.appendChild(labelInput); conditionEl.appendChild(labelLabel); for(let v in condition['vars']) { let varLabel = document.createElement('label'); varLabel.innerHTML = v; let varInput = document.createElement('input'); if(v == 'seconds') { varInput.type = 'number'; } varInput.name = `${condition['@id']}-vars.${v}`; varInput.value = condition['vars'][v]; varInput.addEventListener('change', this.getEditEventListener()); varLabel.appendChild(varInput); conditionEl.appendChild(varLabel); } return conditionEl; } getConditionTypes() { if(typeof this.conditionTypes === 'undefined') { // type: vars: attribtes for crel() this.conditionTypes = { 'timeout': { 'seconds': {'type': 'number', 'value': 10, 'min':0, 'step': 0.1} }, 'replyContains': { 'regex': {'value': '.+'} } } } return this.conditionTypes; } fillConditionFormForType(conditionForm, type) { conditionForm.innerHTML = ""; let vars = this.getConditionTypes()[type]; for(let v in vars){ let attr = vars[v]; attr['name'] = v; conditionForm.appendChild( crel('label', crel('span', v), crel('input', attr) ) ); } } getAddConditionFormEl(direction) { let optionEls = []; let types = this.getConditionTypes(); for(let type in types) { optionEls.push(crel('option', type)); } let conditionForm = crel('div', {'class': 'condition--vars'}); let g = this; let addConditionEl = crel('div', {'class': 'condition condition--add'}, crel('form', { 'on': { 'submit': function(e) { e.preventDefault(); let form = new FormData(e.target); console.log('submit', form); let type = form.get('type'); form.delete('type'); let label = form.get('label'); form.delete('label'); let vars = {}; for(var pair of form.entries()) { vars[pair[0]] = pair[1]; } g.addConditionForDirection(type, label, vars, direction); } } }, crel("h4", "Create New Condition"), crel("label", crel('span', "Type"), crel('select', { 'name': 'type', 'on': { 'change': function(e){ g.fillConditionFormForType(conditionForm, e.target.value); } }}, optionEls), ), crel("label", crel('span', "Description"), crel('input', {'name': 'label'}) ), conditionForm, crel('input', { 'type':'submit', 'value':'create' }) ) ); this.fillConditionFormForType(conditionForm, optionEls[0].value); return addConditionEl; } rmConditionFromDirection(condition, direction) { let id = condition['@id']; // TODO if(typeof direction != 'undefined') { } this._rmNode(id); } getConditionEl(condition) { let conditionEl = document.createElement('div'); return conditionEl; } getDirectionsFrom(msg) { return this.directions.filter(d => d['source'] == msg); } getDirectionsTo(msg) { return this.directions.filter(d => d['target'] == msg); } addMsg() { let msg = { "@id": "n" + Date.now().toString(36), "@type": "Msg", "text": "New", "start": false } this.data.push(msg); this.updateFromData(); this.build(); return msg; } rmMsg(msg) { let invalidatedDirections = this.directions.filter(d => d['source'] == msg || d['target'] == msg); console.log('invalidated', invalidatedDirections); for(let dir of invalidatedDirections) { let i = this.data.indexOf(dir); this.data.splice(i, 1); } this._rmNode(msg); } _rmNode(node) { // remove msg/direction/condition/etc let i = this.data.indexOf(node); this.data.splice(i, 1); this.updateFromData(); this.build(); return this.data; } addConditionForDirection(type, label, vars, direction) { let con = this.addCondition(type, label, vars, true); direction['conditions'].push(con['@id']); this.updateFromData(); this.build(); this.updateMsg(); } addCondition(type, label, vars, skip) { let con = { "@id": "c" + Date.now().toString(36), "@type": "Condition", "type": type, "label": label, "vars": vars } this.data.push(con); if(skip !== true) { this.updateFromData(); this.build(); } return con; } addDirection(source, target) { let dir = { "@id": "d" + Date.now().toString(36), "@type": "Direction", "source": source, "target": target, "conditions": [] } this.data.push(dir); this.updateFromData(); this.build(); return dir; } rmDirection(dir) { this._rmNode(dir); } createMsg() { this.addMsg(); this.build(); } getNodeById(id) { return this.data.filter(node => node['@id'] == id)[0]; } /** * Use wrapper method, because for event handlers 'this' will refer to * the input object */ getEditEventListener(){ let graph = this; let el = function(e){ let parts = e.srcElement.name.split('-'); let id = parts[0], field = parts[1]; console.log(this, graph); let node = graph.getNodeById(id); let path = field.split('.'); // use vars.test to set ['vars']['test'] = value var res=node; for (var i=0;i node['@type'] == 'Msg'); this.directions = this.data.filter((node) => node['@type'] == 'Direction'); this.conditions = this.data.filter((node) => node['@type'] == 'Condition'); this.interruptions = this.data.filter((node) => node['@type'] == 'Interruption'); // save state; this.saveState(); } saveState() { window.localStorage.setItem("lastState", this.getJsonString()); } hasSavedState() { return window.localStorage.getItem("lastState") !== null; } loadFromState() { this.loadData(JSON.parse(window.localStorage.getItem("lastState"))); } build(isInit) { this.simulation = d3.forceSimulation(this.messages) .force("link", d3.forceLink(this.directions).id(d => d['@id'])) .force("charge", d3.forceManyBody().strength(-1000)) .force("center", d3.forceCenter(this.width / 2, this.height / 2)) .force("collide", d3.forceCollide(this.nodeSize*2)) ; // Update existing nodes let node = this.nodesG .selectAll("g") .data(this.messages, n => n['@id']) ; // Update existing nodes let newNode = node.enter(); let newNodeG = newNode.append("g") .attr('id', d => d['@id']) .call(d3.drag(this.simulation)) .on('click', function(d){ this.clickMsg(d); }.bind(this)) ; console.log('a'); let circle = newNodeG.append("circle") .attr('r', this.nodeSize) // .text(d => d.id) ; let text = newNodeG.append("text") ; // remove node.exit().remove(); node = node.merge(newNodeG); // for all existing nodes: node.attr('class', msg => { let classes = []; if( this.selectedMsg == msg) classes.push('selectedMsg'); if( msg['start'] == true ) classes.push('startMsg'); if(this.getDirectionsFrom(msg).length < 1) { classes.push('endMsg'); if(this.getDirectionsTo(msg).length < 1) classes.push('orphanedMsg'); } return classes.join(' '); }) let link = this.linkG .selectAll("line") .data(this.directions) ; let newLink = link.enter() .append("line") ; //remove link.exit().remove(); link = link.merge(newLink); link.attr('class', l => { return `link ` + (l['conditions'].length == 0 ? "link--noconditions" : "link--withconditions"); }); // console.log('c'); let formatText = (t) => { if(t.length > this.maxChars) { return t.substr(0, this.maxChars - 3) + '...'; } else { return t; } }; node.selectAll("text").text(d => formatText(`(${d['@id']}) ${d['text']}`)); // console.log('q'); // // TODO: update text // let text = newNodeG.append("text") // // .attr('stroke', "black") // .text(d => formatText(`(${d['@id']}) ${d['text']}`)) // // .attr('title', d => d.label) // ; let n = this.nodesG; this.simulation.on("tick", () => { link .each(function(d){ let sourceX, targetX, midX, dx, dy, angle; // This mess makes the arrows exactly perfect. // thanks to http://bl.ocks.org/curran/9b73eb564c1c8a3d8f3ab207de364bf4 if( d.source.x < d.target.x ){ sourceX = d.source.x; targetX = d.target.x; } else if( d.target.x < d.source.x ){ targetX = d.target.x; sourceX = d.source.x; } else if (d.target.isCircle) { targetX = sourceX = d.target.x; } else if (d.source.isCircle) { targetX = sourceX = d.source.x; } else { midX = (d.source.x + d.target.x) / 2; if(midX > d.target.x){ midX = d.target.x; } else if(midX > d.source.x){ midX = d.source.x; } else if(midX < d.target.x){ midX = d.target.x; } else if(midX < d.source.x){ midX = d.source.x; } targetX = sourceX = midX; } dx = targetX - sourceX; dy = d.target.y - d.source.y; angle = Math.atan2(dx, dy); // Compute the line endpoint such that the arrow // is touching the edge of the node rectangle perfectly. d.sourceX = sourceX + Math.sin(angle) * this.nodeSize; d.targetX = targetX - Math.sin(angle) * this.nodeSize; d.sourceY = d.source.y + Math.cos(angle) * this.nodeSize; d.targetY = d.target.y - Math.cos(angle) * this.nodeSize; }.bind(this)) .attr("x1", function(d) { return d.sourceX; }) .attr("y1", function(d) { return d.sourceY; }) .attr("x2", function(d) { return d.targetX; }) .attr("y2", function(d) { return d.targetY; }); node.attr("transform", d => `translate(${d.x},${d.y})`); // .attr("cy", d => d.y); }); // this.simulation.alpha(1); // this.simulation.restart(); if(typeof isInit != 'undefined' && isInit) { for (let i = 0, n = Math.ceil(Math.log(this.simulation.alphaMin()) / Math.log(1 - this.simulation.alphaDecay())); i < n; ++i) { this.simulation.tick(); } } return this.svg.node(); } }