hugvey/www/js/hugvey_console.js

746 lines
24 KiB
JavaScript

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 = `<h3>To ${direction['target']['@id']}</h3>`;
} else {
directionEl.innerHTML = `<h3>From ${direction['source']['@id']}</h3>`;
}
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<path.length;i++){
if(i == (path.length -1)) {
console.log('last', path[i]);
res[path[i]] = e.srcElement.value;
} else {
res=res[path[i]];
}
}
// node[field] = e.srcElement.value;
graph.build();
}
return el;
}
getJsonString() {
// recreate array to have the right order of items.
this.data = [...this.messages, ...this.conditions,
...this.directions, ...this.interruptions]
let d = [];
let toRemove = ['sourceX', 'sourceY', 'targetX', 'targetY', 'x','y', 'vx','vy']
for(let node of this.data) {
let n = {};
console.log(node['source']);
for (let e in node) {
if (node.hasOwnProperty(e) && toRemove.indexOf(e) == -1 ) {
if(this.data.indexOf(node[e]) != -1) {
n[e] = node[e]['@id'];
} else {
n[e] = node[e];
}
}
}
d.push(n);
}
return JSON.stringify(d);
}
saveJson() {
var blob = new Blob([this.getJsonString()], {type: 'application/json'});
if(window.navigator.msSaveOrOpenBlob) {
window.navigator.msSaveBlob(blob, "pillow_talk.json");
}
else{
var elem = window.document.createElement('a');
elem.href = window.URL.createObjectURL(blob);
elem.download = "pillow_talk.json";
document.body.appendChild(elem);
elem.click();
document.body.removeChild(elem);
}
}
loadData(data) {
this.data = data;
this.updateFromData();
this.build(true);
}
updateFromData() {
this.messages = this.data.filter((node) => 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();
}
}