hugvey/www/js/hugvey_console.js
2019-02-18 23:13:42 +01:00

1261 lines
45 KiB
JavaScript

var panopticon;
class Panopticon {
constructor() {
console.log( "Init panopticon" );
this.languages = []
// this.selectedHugvey = null;
this.hugveys = new Vue( {
el: "#interface",
data: {
uptime: 0,
languages: [],
hugveys: [],
selectedId: null,
},
methods: {
time_passed: function( hugvey, property ) {
return moment( Date( hugvey[property] * 1000 ) ).fromNow();
},
timer: function(hugvey, property) {
return panopticon.stringToHHMMSS( hugvey[property] );
},
loadNarrative: function( code, file ) {
panopticon.hugveys.selectedId = null;
return panopticon.loadNarrative( code, file );
},
pause: function(hv) {
hv.status = "loading";
return panopticon.pause(hv.id);
},
resume: function(hv) {
hv.status = "loading";
return panopticon.resume(hv.id);
},
restart: function(hv) {
hv.status = "loading";
return panopticon.restart(hv.id);
},
change_lang: function(hv, lang_code) {
hv.status = "loading";
return panopticon.change_language(hv.id, lang_code);
},
showHugvey: function(hv) {
panopticon.hugveys.selectedId = hv.language ? hv.id : null;
panopticon.updateSelectedHugvey();
}
}
} );
this.socket = new ReconnectingWebSocket( "ws://localhost:8888/ws", null, { debug: false, 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':
console.debug(msg);
this.hugveys.uptime = this.stringToHHMMSS( msg['uptime'] );
this.hugveys.languages = msg['languages'];
this.languages = msg['languages'];
this.hugveys.hugveys = msg['hugveys'];
if(this.hugveys.selectedId) {
this.updateSelectedHugvey();
}
break;
}
} );
}
updateSelectedHugvey() {
let hv = null;
if(this.hugveys.selectedId) {
hv = this.getHugvey(this.hugveys.selectedId);
if(hv.language && this.graph.language_code != hv.language) {
this.loadNarrative(hv.language);
}
}
this.graph.updateHugveyStatus(hv);
}
getHugvey(id) {
for(let hv of this.hugveys.hugveys) {
if(hv.id == id) {
return hv;
}
}
return null;
}
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 ) {
if(typeof file == 'undefined') {
for (let lang of this.languages) {
if (lang['code'] == code) {
file = lang['file'];
}
}
}
let req = new XMLHttpRequest();
let graph = this.graph;
req.addEventListener( "load", function( e ) {
graph.loadData( JSON.parse( this.response ), code );
// 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: 'pause', hugvey: hv_id } )
}
restart( hv_id ) {
this.send( { action: 'restart', hugvey: hv_id } );
}
change_language( hv_id, lang_code ) {
this.send( { action: 'change_language', hugvey: hv_id, lang_code: lang_code } );
}
playFromSelected(msg_id) {
if(!this.hugveys.selectedId) {
alert('No hugvey selected');
} else {
this.send({ action: 'play_msg', hugvey: this.hugveys.selectedId, msg_id: msg_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.language_code = 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.diversions = []; // 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 ) {
if ( e.which == "16" ) { // shift
graph.controlDown = true;
document.body.classList.add( 'controlDown' );
}
} );
document.addEventListener( 'keyup', function( e ) {
if ( e.which == "16" ) { // shift
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.
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 );
}
getAudioUrlForMsg(msg) {
let isVariable = msg['text'].includes('$') ? '1' : '0';
return `http://localhost:8888/voice?text=${encodeURIComponent(msg['text'])}&variable=${isVariable}&filename=0`;
}
showMsg( msg ) {
let msgEl = document.getElementById( 'msg' );
msgEl.innerHTML = "";
if(msg == null){
return;
}
let startAttributes = {
'name': msg['@id'] + '-start',
// 'readonly': 'readonly',
'type': 'checkbox',
'on': {
'change': this.getEditEventListener()
}
}
if ( msg['start'] == true ) {
startAttributes['checked'] = 'checked';
}
let audioSrcEl = crel('source', {'src': this.getAudioUrlForMsg(msg)});
let audioSpan = crel(
'span',
{
'title': msg['audio'] ? msg['audio']['file'] : "",
'class': "label-value",
},
crel(
'audio', {'controls': 'controls'},
audioSrcEl
)
);
let msgInfoEl = crel( 'div', { 'class': 'msg__info' },
crel('div', {
'class':'btn btn--delete',
'on': {
'click': function(e) {
if(confirm(`Are you sure you want to remove message ${msg['@id']}`)) {
panopticon.graph.rmMsg(msg);
panopticon.graph.showMsg(null);
}
}
}
}, 'delete'),
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(function(){
audioSrcEl.src = panopticon.graph.getAudioUrlForMsg(msg);
audioSrcEl.parentElement.load();
})
}
} )
),
crel( 'label',
crel( 'span', 'Start' ),
crel( 'input', startAttributes )
),
crel( 'label',
crel( 'span', 'Audio' ),
audioSpan,
crel( 'input', {
'type': 'file',
'name': 'audio',
'accept': '.wav,.ogg,.mp3',
'on': {
'change': function(e) {
audioSpan.innerHTML = "...";
panopticon.graph.saveJson(msg['@id'], e.target, function(e2){
// console.log(e, e2);
audioSpan.innerHTML = e.target.files[0].name + "<sup>*</sup>";
// reload graph:
// console.log('reload', panopticon.graph.language_code);
panopticon.loadNarrative(panopticon.graph.language_code);
});
// console.log(this,e);
}
}
} )
),
crel( 'label',
crel( 'span', {
"title": "The time after the reply in which one can still interrupt to continue speaking"
}, 'Afterrun time' ),
crel( 'input', {
'name': msg['@id'] + '-afterrunTime',
'value': msg['afterrunTime'],
'type': 'number',
'on': {
'change': this.getEditEventListener()
}
} )
)
);
msgEl.appendChild( msgInfoEl );
if(panopticon.hugveys.selectedId) {
let playEl = crel(
'div',
{'class': 'play'},
crel(
'div', {
'class': 'btn btn--play',
'on': {
'click': function (e) {
console.log('go save');
panopticon.graph.saveJson(null, null, function(){
console.log('saved, now play');
panopticon.playFromSelected(msg['@id']);
});
}
}
},
"Save & play on #" + panopticon.hugveys.selectedId
)
);
msgEl.appendChild(playEl);
}
// 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,
crel('div',{
'class': 'btn btn--create',
'on': {
'click': function(e) {
panopticon.graph.createConnectedMsg(msg);
}
}
}, 'Create new message'),
...fromDirections
);
msgEl.appendChild( directionsEl );
}
getDirectionEl( direction, msg ) {
let g = this;
let directionEl = crel('div',
{
'class': 'direction ' + (direction['source'] == msg ? 'dir-to' : 'dir-from'),
'on': {
'mouseover': function(e) {
directionEl.classList.add('dir-highlight');
document.getElementById(direction['@id']).classList.add('dir-highlight');
},
'mouseout': function(e) {
directionEl.classList.remove('dir-highlight');
document.getElementById(direction['@id']).classList.remove('dir-highlight');
}
}
},
crel(
'h3',
{'title': direction['@id']},
direction['source'] == msg ? `To ${direction['target']['text']}`: `From ${direction['source']['text']}`
),
crel('div', {
'class':'btn btn--delete',
'on': {
'click': ( e ) => {
if(confirm("Do you want to remove this direction and its conditions?")) {
g.rmDirection( direction );
}
}
}
}, 'disconnect')
);
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'] ),
crel('div', {
'class':'btn btn--delete',
'on': {
'click': ( e ) => {
if(confirm("Do you want to remove this condition?")) {
console.log('remove condition for direction', condition, direction);
panopticon.graph.rmCondition( condition, direction );
}
}
}
}, 'delete'),
...this.getConditionInputsForType(condition['type'], condition['@id'], condition['vars'])
)
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() {
return {
'timeout': {
'seconds': { 'type': 'number', 'value': 10, 'min': 0, 'step': 0.1, 'unit': "s" },
'onlyIfNoReply': { 'type': 'checkbox', label: "Only if no reply", "title": "This timeout is only used if the participant doesn't say a word. If the participant starts speaking within the time of this timeout condition, only other conditions are applicable." }
},
'replyContains': {
'delays.0.minReplyDuration': { 'type': 'number', 'value': 0, 'min': 0, 'step': 0.1, 'label': 'Delay 1 - reply duration', 'unit': "s", 'readonly': 'readonly' },
'delays.0.waitTime': { 'type': 'number', 'value': 3, 'min': 0, 'step': 0.1 , 'label': 'Delay 1 - wait time', 'unit': "s" },
'delays.1.minReplyDuration': { 'type': 'number', 'value': 5, 'min': 0, 'step': 0.1, 'label': 'Delay 2 - reply duration', 'unit': "s" },
'delays.1.waitTime': { 'type': 'number', 'value': 1, 'min': 0, 'step': 0.1, 'label': 'Delay 2 - time', 'unit': "s" },
'delays.2.minReplyDuration': { 'type': 'number', 'value': 10, 'min': 0, 'step': 0.1, 'label': 'Delay 3 - reply duration', 'unit': "s" },
'delays.2.waitTime': { 'type': 'number', 'value': 0, 'min': 0, 'step': 0.1, 'label': 'Delay 3 - time', 'unit': "s" },
'regex': { 'value': '','placeholder': "match any input" },
'instantMatch': { 'value': '', 'title': "When matched, don't wait for reply to finish. Instantly take this direction.", 'type':'checkbox' },
}
};
}
getConditionInputsForType( type, conditionId, values ) {
let inputs = [];
let vars = this.getConditionTypes()[type];
for ( let v in vars ) {
let attr = vars[v];
attr['name'] = typeof conditionId == 'undefined' ? v : `${conditionId}-vars.${v}`;
if(typeof values != 'undefined') {
let value = this._getValueForPath(v, values);
if(attr['type'] == 'checkbox' ) {
if(value)
attr['checked'] = 'checked';
}
attr['value'] = typeof value == 'undefined' ? "": value;
attr['on'] = {
'change': this.getEditEventListener()
} ;
} else {
// console.log(attr);
}
inputs.push(
crel( 'label',
crel( 'span', {
'title': attr.hasOwnProperty('title') ? attr['title'] : ""
}, attr.hasOwnProperty('label') ? attr['label'] : v ),
crel( 'input', attr )
// crel('span', {'class': 'label-unit'}, attr.hasOwnProperty('unit') ? attr['unit'] : "" )
)
);
}
return inputs;
}
fillConditionFormForType( conditionForm, type, values ) {
conditionForm.innerHTML = "";
let inputs = this.getConditionInputsForType(type);
for(let i of inputs) {
conditionForm.appendChild(i);
}
}
_getValueForPath(path, vars) {
path = path.split( '.' ); // use vars.test to set ['vars']['test'] = value
let v = vars;
let result = null;
for ( let i = 0; i < path.length; i++ ) {
if(!isNaN(parseInt(path[i])) && isFinite(path[i])) {
// is int, use array, instead of obj
path[i] = parseInt(path[i]);
}
v = v[path[i]];
if(i == path.length - 1) {
result = v;
}
if(typeof v == 'undefined') {
break;
}
}
return result;
}
/**
* Save an array path (string) with a value to an object. Used to turn
* strings into nested arrays
* @param string path
* @param {any} value
* @param array|object vars
*/
_formPathToVars(path, value, vars) {
path = path.split( '.' ); // use vars.test to set ['vars']['test'] = value
let res = vars;
for ( let i = 0; i < path.length; i++ ) {
if ( i == ( path.length - 1 ) ) {
res[path[i]] = value;
} else {
if(!isNaN(parseInt(path[i+1])) && isFinite(path[i+1])) {
// is int, use array, instead of obj
path[i+1] = parseInt(path[i+1]);
}
if(typeof res[path[i]] == 'undefined') {
res[path[i]] = typeof path[i+1] == 'number' ? [] : {}
}
res = res[path[i]];
}
}
return vars;
}
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' );
// checkboxes to true/false
let defs = g.getConditionTypes()[type];
console.log(defs);
for(let field in defs) {
console.log(field);
if(defs[field]['type'] == 'checkbox') {
console.info('configure checkbox', field);
form.set(field, form.has(field));
}
}
let vars = {};
for ( var pair of form.entries() ) {
// FormData only has strings & blobs, we want booleans:
if(pair[1] === 'true') pair[1] = true;
if(pair[1] === 'false') pair[1] = false;
vars = g._formPathToVars(pair[0], pair[1], vars);
}
// TODO: checkboxes
console.log("Createded", vars);
g.addConditionForDirection( type, label, vars, direction );
}
}
},
crel( "h4", {
'class': "divToggle",
'on': {
'click': function(e) { this.classList.toggle('opened'); }
}
}, "Create New Condition" ),
crel('div', {'class': 'divToggle-target'},
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;
}
/**
* remove condition from the graph or merely from the given direction
* @param {any} condition The condition to remove
* @param {any} direction if given, only remove from this direction
*/
rmCondition( condition, direction ) {
let id = condition['@id'];
// TODO
if ( typeof direction != 'undefined' ) {
let pos = direction['conditions'].indexOf(id);
console.log('delete', id, 'on direction');
if(pos > -1) {
direction['conditions'].splice(pos, 1);
}
for(let dir of this.directions) {
// console.log('check if condition exists for dir', dir)
if(dir['conditions'].indexOf(id) > -1) {
console.log("Condition still in use");
this.updateFromData();
this.build();
this.updateMsg();
return;
}
}
console.log('No use, remove', condition)
this._rmNode( condition );
} else {
for(let dir of this.directions) {
let pos = dir['conditions'].indexOf(id);
if(pos > -1) {
dir['conditions'].splice(pos, 1);
}
}
console.log('remove condition?', id)
this._rmNode( condition );
}
this.updateMsg();
}
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": this.language_code.substring( 0, 2 ) + "-n" + Date.now().toString( 36 ),
"@type": "Msg",
"text": "New",
"start": false,
"afterrunTime": 0.5,
}
this.data.push( msg );
this.updateFromData();
this.build();
this.selectMsg(msg);
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": this.language_code.substring( 0, 2 ) + "-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": this.language_code.substring( 0, 2 ) + "-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 );
// todo, remove orphaned conditions
}
createMsg() {
this.addMsg();
this.build();
}
createConnectedMsg(sourceMsg) {
let newMsg = this.addMsg();
this.addDirection(sourceMsg, newMsg);
this.build();
// reselect so that overview is updated
this.selectMsg(newMsg);
}
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(callback) {
let graph = this;
let el = function( e ) {
console.info("Changed", e);
let parts = e.srcElement.name.split( '-' );
let field = parts.pop();
let id = parts.join('-');
let node = graph.getNodeById( id );
let path = field.split( '.' ); // use vars.test to set ['vars']['test'] = value
var res = node;
let value = e.srcElement.value
if(e.srcElement.type == 'checkbox') {
value = e.srcElement.checked;
}
for ( var i = 0; i < path.length; i++ ) {
if ( i == ( path.length - 1 ) ) {
res[path[i]] = value;
} else {
res = res[path[i]];
}
}
// node[field] = e.srcElement.value;
graph.build();
if(typeof callback !== 'undefined'){
callback();
}
}
return el;
}
getJsonString() {
// recreate array to have the right order of items.
this.data = [...this.messages, ...this.conditions,
...this.directions, ...this.diversions]
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 );
}
console.info("Jsonified graph:",d);
return JSON.stringify( d );
}
downloadJson() {
if ( !this.language_code ) {
alert( "Make sure to load a language first" )
}
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 );
}
}
saveJson( msg_id, fileInputElement, callback ) {
if ( !this.language_code ) {
alert( "Make sure to load a language first" )
}
let formData = new FormData();
formData.append( "language", this.language_code );
if ( msg_id ) {
formData.append( "message_id", msg_id );
formData.append( "audio", fileInputElement.files[0] );
}
let blob = new Blob( [this.getJsonString()], { type: "application/json" } );
formData.append( "json", blob );
console.info("Save json", formData );
var request = new XMLHttpRequest();
request.open( "POST", "http://localhost:8888/upload" );
if(callback) {
request.addEventListener( "load", callback);
}
request.send( formData );
}
loadData( data, language_code ) {
this.language_code = language_code;
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.diversions = this.data.filter(( node ) => node['@type'] == 'Diversion' );
document.getElementById('current_lang').innerHTML = "";
document.getElementById('current_lang').appendChild(crel('span', {
'class': 'flag-icon ' + this.language_code
}));
this.distances = this.calculateDistancesFromStart();
// save state;
this.saveState();
}
updateHugveyStatus(hv) {
let els = document.getElementsByClassName('beenHit');
while(els.length > 0) {
els[0].classList.remove('beenHit');
}
if(!hv || typeof hv['history'] == 'undefined') {
return;
}
for(let msg of hv['history']['messages']) {
document.getElementById(msg[0]['id']).classList.add('beenHit');
}
for(let msg of hv['history']['directions']) {
document.getElementById(msg[0]['id']).classList.add('beenHit');
}
}
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'] ).strength(0) )
// .force( "charge", d3.forceManyBody().strength( 100 ) )
// .force( "center", d3.forceCenter( this.width / 2, this.height / 2 ) )
.force( "collide", d3.forceCollide( this.nodeSize * 2.3 ).strength(2) )
.force( "forceX", d3.forceX(function(m){
let fx = panopticon.graph.distances[m['@id']] !== null ? panopticon.graph.distances[m['@id']] * panopticon.graph.nodeSize * 4 : 0
console.log('fx', m['@id'], panopticon.graph.distances[m['@id']], fx);
return fx;
}).strength(50))
.force( "forceY", d3.forceY(m => panopticon.graph.distances[m['@id']] !== null ? 0 : panopticon.graph.nodeSize * 3 ).strength(30))
;
this.simulation.velocityDecay(.99);
// 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 ) )
;
let circle = newNodeG.append( "circle" )
.attr( 'r', this.nodeSize )
// .text(d => d.id)
;
let textId = newNodeG.append( "text" ).attr( 'class', 'msg_id' );
let textContent = newNodeG.append( "text" ).attr( 'class', 'msg_txt' );
let statusIcon = newNodeG.append( "image" )
.attr( 'class', 'status_icon' )
.attr( 'x', '-10' )
.attr( 'y', '10' )
.attr( 'width', '20' )
.attr( 'height', '20' )
;
// 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" ); } );
link.attr('id', (l) => l['@id']);
let formatText = ( t ) => {
if ( t.length > this.maxChars ) {
return t.substr( 0, this.maxChars - 3 ) + '...';
} else {
return t;
}
};
node.selectAll( "text.msg_id" ).text( d => d['@id'] );
node.selectAll( "text.msg_txt" ).text( d => formatText( `${d['text']}` ) );
node.selectAll( "image.status_icon" ).attr('xlink:href', d => d['audio'] ? '' : '/images/music-broken.svg');
// 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();
}
calculateDistancesFromStart() {
let starts = this.messages.filter( m => m.hasOwnProperty('start') && m['start'] == true);
if (starts.length < 1) {
console.error("No start set");
return;
}
let startMsg = starts[0];
//initiate distances
let distances = {};
for(let msg of this.messages) {
distances[msg['@id']] = msg === startMsg ? 0 : null;
}
let targetsPerMsg = {};
let sourcesPerMsg = {};
console.log("dir", this.directions);
for(let direction of this.directions) {
let from = typeof direction['source'] == "string" ? direction['source'] : direction['source']['@id'];
let to = typeof direction['target'] == "string" ? direction['target'] : direction['target']['@id'];
if(!targetsPerMsg.hasOwnProperty(from)) {
targetsPerMsg[from] = [];
}
targetsPerMsg[from].push(to);
if(!sourcesPerMsg.hasOwnProperty(to)) {
sourcesPerMsg[to] = [];
}
sourcesPerMsg[to].push(from);
}
let traverseMsg = function(msgId, depth, goingDown) {
let msgsPerMsg = goingDown ? targetsPerMsg : sourcesPerMsg;
console.log(goingDown, msgId, depth);
if(!msgsPerMsg.hasOwnProperty(msgId)) {
// end of trail
return;
}
for(let childMsgId of msgsPerMsg[msgId]) {
if(distances[childMsgId] === null || (goingDown && distances[childMsgId] > depth)) {
distances[childMsgId] = depth;
// console.log(goingDown, childMsgId, depth);
traverseMsg(childMsgId, goingDown ? (depth+1) : (depth - 1), goingDown);
}
else if(!goingDown && distances[childMsgId] < depth) {
// console.log('a', depth);
traverseMsg(childMsgId, depth - 1, goingDown);
} else {
// apparently, there is a loop. Don't traverse it.
}
}
}
traverseMsg(startMsg['@id'], 1 , true);
// now we have the formal tree, lets try to polish the rest:
for(let msgId in distances) {
if(distances[msgId] === null) {
continue;
}
// let's see if there are parent nodes that are not in the distances array
// traverse up and see whether we encounter anything new
traverseMsg(msgId, distances[msgId] -1, false)
}
// let additionalsDepth = 0;
//// now the secondary strands:
// for(let msgId in distances) {
// if(distances[msgId] !== null || sourcesPerMsg.hasOwnProperty(msgId)) {
// // it is already calculated, or it has a parent node (which we should traverse instead)
// continue;
// }
// distances[msgId] = additionalsDepth;
// traverseMsg(msgId, additionalsDepth+1, true);
//
// }
return distances;
}
}