1624 lines
56 KiB
JavaScript
1624 lines
56 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 / 16, 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(); } );
|
|
document.getElementById( 'btn-diversions' ).addEventListener( 'click', function( e ) { graph.showDiversions(); } );
|
|
}
|
|
|
|
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`;
|
|
}
|
|
|
|
getNumericId(prefix) {
|
|
let id, i = 0;
|
|
let hasId= function(a, id) {
|
|
for(let i of a){
|
|
if(i['@id'] == id) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
do {
|
|
id = prefix + i;
|
|
i++;
|
|
} while(hasId(this.data, id))
|
|
|
|
return id;
|
|
}
|
|
|
|
createDiversion(type) {
|
|
let div = {
|
|
"@id": this.getNumericId(this.language_code.substring( 0, 2 ) + `-div-${type}#`),
|
|
'@type': 'Diversion',
|
|
'type': type,
|
|
'params': {}
|
|
}
|
|
|
|
if(type == 'no_response') {
|
|
div['params']['consecutiveSilences'] = 3;
|
|
div['params']['timesOccured'] = 0;
|
|
div['params']['returnAfterStrand'] = true;
|
|
div['params']['msgId'] = "";
|
|
}
|
|
else if(type == 'repeat') {
|
|
div['params']['regex'] = "can you repeat that\\?";
|
|
} else {
|
|
console.log("invalid type", type);
|
|
alert('invalid type for diversion');
|
|
}
|
|
|
|
this.data.push( div );
|
|
this.updateFromData();
|
|
this.build();
|
|
|
|
this.showDiversions();
|
|
return msg;
|
|
}
|
|
|
|
deleteDiversion(div) {
|
|
this._rmNode( div );
|
|
this.showDiversions( );
|
|
}
|
|
|
|
showDiversions( ) {
|
|
let msgEl = document.getElementById( 'msg' );
|
|
msgEl.innerHTML = "";
|
|
|
|
let divsNoResponse =[], divsRepeat = [];
|
|
for(let div of this.diversions) {
|
|
if(div['type'] == 'no_response') {
|
|
let returnAttrs = {
|
|
'type': 'checkbox',
|
|
'on': {
|
|
'change': (e) => div['params']['returnAfterStrand'] = e.target.checked
|
|
}
|
|
}
|
|
if(div['params']['returnAfterStrand']) {
|
|
returnAttrs['checked'] = 'checked';
|
|
}
|
|
let msgOptions = [crel('option',"")];
|
|
let starts = this.messages.filter( m => m.hasOwnProperty('start') && m['start'] == true);
|
|
for(let startMsg of starts) {
|
|
let optionParams = {};
|
|
if(div['params']['msgId'] == startMsg['@id']) {
|
|
optionParams['selected'] = 'selected';
|
|
}
|
|
msgOptions.push(crel('option', optionParams , startMsg['@id']));
|
|
}
|
|
|
|
divsNoResponse.push(crel(
|
|
'div', {
|
|
'class': 'diversion',
|
|
'on': {
|
|
'mouseover': function(e) {
|
|
if(div['params']['msgId'])
|
|
document.getElementById(div['params']['msgId']).classList.add('selectedMsg');
|
|
},
|
|
'mouseout': function(e) {
|
|
if(div['params']['msgId'])
|
|
document.getElementById(div['params']['msgId']).classList.remove('selectedMsg');
|
|
}
|
|
}
|
|
},
|
|
crel('h3', div['@id']),
|
|
crel(
|
|
'div', {
|
|
'class':'btn btn--delete',
|
|
'on': {
|
|
'click': (e) => this.deleteDiversion(div)
|
|
}
|
|
}, 'Delete diversion'),
|
|
crel('label', 'Consecutive Silences',
|
|
crel('input', {
|
|
'type': 'number',
|
|
'value': div['params']['consecutiveSilences'],
|
|
'on': {
|
|
'change': (e) => div['params']['consecutiveSilences'] = parseInt(e.target.value)
|
|
}
|
|
})
|
|
),
|
|
crel('label', 'On n-th instance',
|
|
crel('input', {
|
|
'type': 'number',
|
|
'value': div['params']['timesOccured'],
|
|
'on': {
|
|
'change': (e) => div['params']['timesOccured'] = parseInt(e.target.value)
|
|
}
|
|
})
|
|
),
|
|
crel('label', 'Return to point of departure afterwards',
|
|
crel('input', returnAttrs)
|
|
),
|
|
crel('label', 'Go to (start message)',
|
|
crel('select', {'on': {
|
|
'change': (e) => div['params']['msgId'] = e.target.value
|
|
}}, ...msgOptions)
|
|
)
|
|
));
|
|
}
|
|
if(div['type'] == 'repeat'){
|
|
divsRepeat.push(crel(
|
|
'div', {'class': 'diversion'},
|
|
crel('h3', div['@id']),
|
|
crel(
|
|
'div', {
|
|
'class':'btn btn--delete',
|
|
'on': {
|
|
'click': (e) => this.deleteDiversion(div)
|
|
}
|
|
}, 'Delete diversion'),
|
|
crel('label', 'Regex',
|
|
crel('input', {
|
|
'type': 'text',
|
|
'value': div['params']['regex'],
|
|
'on': {
|
|
'change': (e) => div['params']['regex'] = e.target.value
|
|
}
|
|
})
|
|
)
|
|
));
|
|
}
|
|
}
|
|
|
|
console.log(divsNoResponse, divsRepeat);
|
|
|
|
let divEl = crel(
|
|
'div',
|
|
{
|
|
'id': 'diversions'
|
|
},
|
|
crel('h1', 'Configure Diversions'),
|
|
crel('div',
|
|
crel('h2', 'In case of No Response'),
|
|
...divsNoResponse,
|
|
crel('div',
|
|
{
|
|
'class': 'btn',
|
|
'on': {
|
|
'click': (e) => this.createDiversion('no_response')
|
|
}
|
|
},
|
|
'New case for no_response'
|
|
)
|
|
),
|
|
crel('div',
|
|
crel('h2', 'Request repeat'),
|
|
...divsRepeat,
|
|
crel('div',
|
|
{
|
|
'class': 'btn',
|
|
'on': {
|
|
'click': (e) => this.createDiversion('repeat')
|
|
}
|
|
},
|
|
'New case for repeat'
|
|
)
|
|
)
|
|
);
|
|
|
|
msgEl.appendChild(divEl);
|
|
}
|
|
|
|
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 beginningAttributes = {
|
|
'name': msg['@id'] + '-beginning',
|
|
// 'readonly': 'readonly',
|
|
'type': 'checkbox',
|
|
'on': {
|
|
'change': this.getEditEventListener()
|
|
}
|
|
}
|
|
if ( msg['beginning'] == true ) {
|
|
beginningAttributes['checked'] = 'checked';
|
|
}
|
|
|
|
let params = {};
|
|
if(msg.hasOwnProperty('params')) {
|
|
params = msg['params'];
|
|
} else {
|
|
msg['params'] = {};
|
|
}
|
|
|
|
let audioSrcEl = crel('source', {'src': msg['audio'] ? msg['audio']['file'] : this.getAudioUrlForMsg(msg)});
|
|
// console.log(msg['audio']);
|
|
let audioSpan = crel(
|
|
'span',
|
|
{
|
|
'title': msg['audio'] ? msg['audio']['file'] : "",
|
|
'class': "label-value",
|
|
},
|
|
crel(
|
|
'audio', {'controls': 'controls'},
|
|
audioSrcEl
|
|
),
|
|
crel('div', msg['audio'] ? crel(
|
|
'div',
|
|
crel('div', {
|
|
'class':'btn btn--delete',
|
|
'on': {
|
|
'click': function(e) {
|
|
e.stopPropagation();
|
|
e.preventDefault();
|
|
panopticon.graph.getNodeById(msg['@id'])['audio'] = null;
|
|
panopticon.graph.showMsg(msg);
|
|
}
|
|
}
|
|
}, 'del'),
|
|
"uploaded"
|
|
) : 'Auto-generated')
|
|
);
|
|
let msgInfoEl = crel( 'div', { 'class': 'msg__info' },
|
|
crel('div', {
|
|
'class':'btn btn--delete btn--delete-msg',
|
|
'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'] + ` (${panopticon.graph.distances[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', 'Beginning' ),
|
|
crel( 'input', beginningAttributes )
|
|
),
|
|
|
|
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',
|
|
'step': "0.1",
|
|
'value': msg['afterrunTime'],
|
|
'type': 'number',
|
|
'on': {
|
|
'change': this.getEditEventListener()
|
|
}
|
|
} )
|
|
),
|
|
crel( 'label',
|
|
crel( 'span', {
|
|
"title": "Playback volume factor"
|
|
}, 'Volume factor' ),
|
|
crel( 'input', {
|
|
'name': msg['@id'] + '-params.vol',
|
|
'value': params.hasOwnProperty('vol') ? params['vol'] : 1,
|
|
'step': "0.1",
|
|
'type': 'number',
|
|
'on': {
|
|
'change': this.getEditEventListener()
|
|
}
|
|
} )
|
|
),
|
|
crel( 'label',
|
|
crel( 'span', {
|
|
"title": "Playback tempo factor"
|
|
}, 'Tempo factor' ),
|
|
crel( 'input', {
|
|
'name': msg['@id'] + '-params.tempo',
|
|
'value': params.hasOwnProperty('tempo') ? params['tempo'] : 1,
|
|
'step': "0.1",
|
|
'min': "0.1",
|
|
'type': 'number',
|
|
'on': {
|
|
'change': this.getEditEventListener()
|
|
}
|
|
} )
|
|
),
|
|
crel( 'label',
|
|
crel( 'span', {
|
|
"title": "Playback pitch factor"
|
|
}, 'Pitch factor' ),
|
|
crel( 'input', {
|
|
'name': msg['@id'] + '-params.pitch',
|
|
'value': params.hasOwnProperty('pitch') ? params['pitch'] : 0,
|
|
'step': "0.1",
|
|
'type': 'number',
|
|
'on': {
|
|
'change': this.getEditEventListener()
|
|
}
|
|
} )
|
|
),
|
|
|
|
// color for beter overview
|
|
|
|
crel( 'label',
|
|
crel( 'span', {
|
|
"title": "Color - for your eyes only"
|
|
}, 'Color' ),
|
|
crel( 'input', {
|
|
'name': msg['@id'] + '-color',
|
|
'value': msg.hasOwnProperty('color') ? msg['color'] : '#77618e',
|
|
'type': 'color',
|
|
'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.getNodeById(newMsg['@id']).y = this.getNodeById(sourceMsg['@id']).y;
|
|
|
|
if(this.getNodeById(sourceMsg['@id']).hasOwnProperty('color')){
|
|
this.getNodeById(newMsg['@id']).color = this.getNodeById(sourceMsg['@id']).color
|
|
}
|
|
|
|
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']
|
|
let toRemove = ['sourceX', 'sourceY', 'targetX', 'targetY', '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 * 1.5 ).strength(3) )
|
|
.force( "forceX", d3.forceX(function(m){
|
|
let fx = panopticon.graph.distances[m['@id']] !== null ? panopticon.graph.distances[m['@id']][0] * panopticon.graph.nodeSize * 4 : 0
|
|
// console.log('fx', m['@id'], panopticon.graph.distances[m['@id']], fx);
|
|
return fx;
|
|
}).strength(50))
|
|
.force( "forceY", d3.forceY(function(m){
|
|
// if(panopticon.graph.distances[m['@id']] !== null )
|
|
// console.log(panopticon.graph.distances[m['@id']][1]);
|
|
let fy = panopticon.graph.distances[m['@id']] !== null ? panopticon.graph.distances[m['@id']][1] * panopticon.graph.nodeSize * 3: 0
|
|
// console.log('fx', m['@id'], panopticon.graph.distances[m['@id']], fx);
|
|
return fy;
|
|
}).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'] )
|
|
.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( ' ' );
|
|
} )
|
|
|
|
.on(".drag", null)
|
|
.call(
|
|
d3.drag( this.simulation )
|
|
.on("start", function(d){
|
|
if (!d3.event.active) panopticon.graph.simulation.alphaTarget(0.3).restart();
|
|
d.fx = d.x;
|
|
d.fy = d.y;
|
|
})
|
|
.on('drag', function(d){
|
|
d.fx = d3.event.x;
|
|
d.fy = d3.event.y;
|
|
})
|
|
.on("end", function(d){
|
|
if (!d3.event.active) panopticon.graph.simulation.alphaTarget(0);
|
|
d.fx = null;
|
|
d.fy = null;
|
|
})
|
|
// .container(document.getElementById('container'))
|
|
|
|
);
|
|
|
|
node.select('circle').attr('style', (d) => 'fill: ' + (d.hasOwnProperty('color') ? d['color'] : '#77618e'));
|
|
|
|
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();
|
|
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;
|
|
}
|
|
|
|
//initiate distances
|
|
let distances = {};
|
|
for(let msg of this.messages) {
|
|
// distances[msg['@id']] = msg === startMsg ? 0 : null;
|
|
distances[msg['@id']] = 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, yPos) {
|
|
let msgsPerMsg = goingDown ? targetsPerMsg : sourcesPerMsg;
|
|
// console.log(goingDown, msgId, depth);
|
|
if(!msgsPerMsg.hasOwnProperty(msgId)) {
|
|
// end of trail
|
|
return yPos;
|
|
}
|
|
|
|
|
|
let i = 0, y =0;
|
|
for(let childMsgId of msgsPerMsg[msgId]) {
|
|
if(distances[childMsgId] === null || (goingDown && distances[childMsgId][0] > depth)) {
|
|
|
|
if(distances[childMsgId] === null) {
|
|
if(i > 0){
|
|
yPos++;
|
|
}
|
|
i++;
|
|
|
|
console.log('set for id', childMsgId, goingDown, depth, yPos);
|
|
distances[childMsgId] = [depth, yPos];
|
|
|
|
}
|
|
else{
|
|
y++;
|
|
}
|
|
// console.log(goingDown, childMsgId, depth);
|
|
yPos = traverseMsg(childMsgId, goingDown ? (depth+1) : (depth - 1), goingDown, yPos);
|
|
}
|
|
else if(!goingDown && distances[childMsgId][0] < depth) {
|
|
if(childMsgId == 'en-njsgkr4az') {
|
|
console.log('set for id', childMsgId, goingDown);
|
|
}
|
|
if(distances[childMsgId] === null) {
|
|
distances[childMsgId] = [depth, yPos];
|
|
}
|
|
|
|
// console.log('a', depth);
|
|
yPos = traverseMsg(childMsgId, depth - 1, goingDown, yPos);
|
|
} else {
|
|
// apparently, there is a loop. Don't traverse it.
|
|
}
|
|
|
|
}
|
|
|
|
// if( i == 0 && y == 1) {
|
|
// // we reached an item that branches back into the tree
|
|
// return yPos -1;
|
|
// }
|
|
// console.log('yPos',msgId,yPos);
|
|
return yPos;
|
|
}
|
|
|
|
let yPos = 0;
|
|
for(let startMsg of starts) {
|
|
if(distances[startMsg['@id']] === null) {
|
|
distances[startMsg['@id']] = [0, yPos];
|
|
}
|
|
yPos = traverseMsg(startMsg['@id'], 1 , true, yPos);
|
|
yPos += 1;
|
|
}
|
|
|
|
// 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][0] -1, false, distances[msgId][1])
|
|
}
|
|
|
|
// 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;
|
|
}
|
|
}
|
|
|