hugvey/www/js/hugvey_console.js
Hugvey Central Command 480e8a49bc Fix onchangelistener to use e.target
For some reason the change listener for the edit fields used e.srcElement, this didn't seem to work everywhere.
Using e.target should fix this.
2019-05-13 14:12:54 +02:00

2155 lines
75 KiB
JavaScript

var panopticon;
class Panopticon {
constructor() {
console.log( "Init panopticon" );
this.hasGraph = document.body.classList.contains('editor');
this.languages = []
// this.selectedHugvey = null;
this.hugveys = new Vue( {
el: "#interface",
data: {
uptime: 0,
languages: [],
hugveys: [],
selectedId: null,
logbook: "",
logbookId: null,
},
methods: {
time_passed: function( hugvey, property ) {
return moment( Date( hugvey[property] * 1000 ) ).fromNow();
},
timer: function(hugvey, property) {
return panopticon.stringToHHMMSS( hugvey[property] );
},
formatted: function(time) {
return moment(time).utc().format("hh:mm:ss");
},
loadNarrative: function( code, file ) {
panopticon.hugveys.selectedId = null;
if(panopticon.hasGraph) {
return panopticon.loadNarrative( code, file );
}
},
block: function(hv) {
hv.status = "loading";
return panopticon.block(hv.id);
},
unblock: function(hv) {
hv.status = "loading";
return panopticon.unblock(hv.id);
},
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);
},
finish: function(hv) {
hv.status = "loading";
return panopticon.finish(hv.id);
},
change_lang: function(hv, lang_code) {
hv.status = "loading";
return panopticon.change_language(hv.id, lang_code);
},
change_light: function(e) {
let hv_id = parseInt(e.target.dataset.hvid);
let light_id = parseInt(e.target.value);
console.log(hv_id, light_id, this);
return panopticon.change_light_id(hv_id, light_id);
},
showHugvey: function(hv) {
panopticon.hugveys.selectedId = hv.language ? hv.id : null;
panopticon.hugveys.logbook = [];
panopticon.hugveys.logbookId = null;
panopticon.updateSelectedHugvey();
}
}
} );
this.socket = new ReconnectingWebSocket( "ws://localhost:8888/ws", null, { debug: false, reconnectInterval: 3000 } );
if(this.hasGraph) {
this.graph = new Graph();
}
this.socket.addEventListener( 'open', ( e ) => {
this.send( { action: 'init' } );
} );
// request close before unloading
window.addEventListener('beforeunload', function(){
panopticon.socket.close();
});
this.socket.addEventListener( 'close', function( e ) {
console.log( 'Closed connection' );
} );
this.socket.addEventListener( 'message', ( e ) => {
if(e.data == 'hello!') {
console.log("Websocket connected")
return;
}
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;
}
console.debug(msg);
switch ( msg['action'] ) {
case 'status':
this.hugveys.uptime = this.stringToHHMMSS( msg['uptime'] );
this.hugveys.languages = msg['languages'];
this.languages = msg['languages'];
this.hugveys.hugveys = msg['hugveys'];
this.hugveys.logbook = msg['logbook'];
this.hugveys.logbookId = msg['logbookId'];
if(this.hugveys.selectedId) {
this.updateSelectedHugvey();
}
break;
case 'log':
break;
}
} );
}
updateSelectedHugvey() {
let hv = null;
if(this.hugveys.selectedId) {
hv = this.getHugvey(this.hugveys.selectedId);
if(this.hasGraph) {
if(hv.language && this.graph.language_code != hv.language) {
this.loadNarrative(hv.language);
}
}
// let varEl = document.getElementById("variables");
// varEl.innerHTML = "";
}
if(this.hasGraph) {
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', selected_id: panopticon.hugveys.selectedId } );
}
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();
}
block( hv_id ) {
this.send( { action: 'block', hugvey: hv_id } )
}
unblock( hv_id ) {
this.send( { action: 'unblock', hugvey: hv_id } )
}
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 } );
}
finish( hv_id ) {
this.send( { action: 'finish', hugvey: hv_id } );
}
change_language( hv_id, lang_code ) {
this.send( { action: 'change_language', hugvey: hv_id, lang_code: lang_code } );
}
change_light_id( hv_id, light_id ) {
console.log("Light", hv_id, light_id);
this.send( { action: 'change_light', hugvey: hv_id, light_id: light_id } );
}
playFromSelected(msg_id, reloadStory) {
if(!this.hugveys.selectedId) {
alert('No hugvey selected');
} else {
this.send({ action: 'play_msg', hugvey: this.hugveys.selectedId, msg_id: msg_id, reloadStory: reloadStory })
}
}
}
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(); } );
document.getElementById( 'btn-audio' ).addEventListener( 'click', function( e ) { graph.showAudioFiles(); } );
document.getElementById( 'btn-config' ).addEventListener( 'click', function( e ) { graph.showConfiguration(); } );
}
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';
let lang = panopticon.graph.language_code;
return `http://localhost:8888/voice?text=${encodeURIComponent(msg['text'])}&variable=${isVariable}&lang=${lang}&filename=0`;
}
getConfig() {
}
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 == 'reply_contains') {
div['params']['regex'] = "";
div['params']['returnAfterStrand'] = true;
div['params']['msgId'] = "";
div['params']['notForColor'] = "";
div['params']['waitTime'] = 1.8;
}
else if(type == 'interrupt') {
div['params']['msgId'] = "";
}
else if(type == 'timeout') {
div['params']['interval'] = 20;
div['params']['timesOccured'] = 0;
div['params']['minTimeAfterMessage'] = 2.;
div['params']['fromLastMessage'] = false;
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');
}
if(type != 'repeat' && type != 'interrupt') {
div['params']['notAfterMsgId'] = "";
}
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 = [], divsReplyContains = [], divsTimeouts = [], divsInterrupts = [];
for(let div of this.diversions) {
let notAfterMsgIdEl = "";
if(div['type'] != 'repeat') {
let notMsgOptions = [crel('option',"")];
let chapterMsgs = this.messages.filter( m => m.hasOwnProperty('chapterStart') && m['chapterStart'] == true);
for(let startMsg of chapterMsgs) {
let optionParams = {
'value': startMsg['@id']
};
if(div['params']['notAfterMsgId'] == startMsg['@id']) {
optionParams['selected'] = 'selected';
}
notMsgOptions.push(crel('option', optionParams , `${startMsg['text']} (${startMsg['@id']})`));
}
notAfterMsgIdEl = crel('label', 'Not when chapter has hit:',
crel('select', {'on': {
'change': (e) => div['params']['notAfterMsgId'] = e.target.value
}}, ...notMsgOptions)
);
}
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)
),
notAfterMsgIdEl
));
}
if(div['type'] == 'reply_contains') {
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']));
}
divsReplyContains.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', 'Regex',
crel('input', {
'type': 'text',
'value': div['params']['regex'],
'placeholder': 'regex',
'on': {
'change': (e) => div['params']['regex'] = e.target.value
}
})
),
crel('label', 'Ignore for color',
crel('input', {
'type': 'text', // use text instead of color, as color doesn't allow for empty values
'value': typeof div['params']['notForColor'] !== 'undefined' ? div['params']['notForColor'] : "",
'on': {
'change': function(e) {
if(e.target.value.length > 0 && e.target.value.substr(0,1) !== '#') {
alert("Don't forget to have a valid hex including the #-character, eg: #00ff00");
}
div['params']['notForColor'] = 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)
),
crel('label', 'Wait time',
crel('input', {
'type': 'number',
'step': 0.1,
'value': div['params']['waitTime'],
'on': {
'change': (e) => div['params']['waitTime'] = parseFloat(e.target.value)
}
})
),
notAfterMsgIdEl
));
}
if(div['type'] == 'timeout') {
let returnAttrs = {
'type': 'checkbox',
'on': {
'change': (e) => div['params']['returnAfterStrand'] = e.target.checked
}
}
if(div['params']['returnAfterStrand']) {
returnAttrs['checked'] = 'checked';
}
let totalOrLocalAttrs = {
'type': 'checkbox',
'on': {
'change': (e) => div['params']['fromLastMessage'] = e.target.checked
}
}
if(div['params']['fromLastMessage']) {
totalOrLocalAttrs['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']));
}
divsTimeouts.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', 'For last message only',
crel('input', totalOrLocalAttrs)
),
crel('label', 'Seconds of silence',
crel('input', {
'type': 'number',
'value': div['params']['interval'],
'precision': .1,
'on': {
'change': (e) => div['params']['interval'] = parseFloat(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', 'Minimum time after message',
crel('input', {
'type': 'number',
'value': div['params']['minTimeAfterMessage'],
'on': {
'change': (e) => div['params']['minTimeAfterMessage'] = parseFloat(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)
),
notAfterMsgIdEl
));
}
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
}
})
)
));
}
if(div['type'] == 'interrupt'){
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']));
}
divsInterrupts.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', 'Go to (start message)',
crel('select', {'on': {
'change': (e) => div['params']['msgId'] = e.target.value
}}, ...msgOptions)
)
));
}
}
console.log(divsReplyContains, divsNoResponse, divsRepeat, divsTimeouts, divsInterrupts);
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', 'Reply Contains'),
...divsReplyContains,
crel('div',
{
'class': 'btn',
'on': {
'click': (e) => this.createDiversion('reply_contains')
}
},
'New case for reply contains'
)
),
crel('div',
crel('h2', 'Request repeat'),
...divsRepeat,
crel('div',
{
'class': 'btn',
'on': {
'click': (e) => this.createDiversion('repeat')
}
},
'New case for repeat'
)
),
crel('div',
crel('h2', 'Timeouts'),
...divsTimeouts,
crel('div',
{
'class': 'btn',
'on': {
'click': (e) => this.createDiversion('timeout')
}
},
'New case for timeout'
)
)
// ,
// crel('div',
// crel('h2', 'Interruptions (random pick)'),
// ...divsInterrupts,
// crel('div',
// {
// 'class': 'btn',
// 'on': {
// 'click': (e) => this.createDiversion('interrupt')
// }
// },
// 'New case for Interrupt'
// )
// )
);
msgEl.appendChild(divEl);
}
showAudioFiles( ) {
let audioFilesEl = crel('div',{
'id':'audioFiles'
},
crel(
'div',
{
'class':'btn btn-close',
'on': {
'click': function() {
audioFilesEl.parentNode.removeChild(audioFilesEl)
}
}
},
'close'
));
for(let msg of panopticon.graph.messages) {
audioFilesEl.appendChild(crel(
'audio',
{'controls':'controls'},
crel('source', {'src': msg['audio'] ? msg['audio']['file'] : panopticon.graph.getAudioUrlForMsg(msg)})
))
}
document.getElementById("interface").appendChild(audioFilesEl);
}
showConfiguration( ) {
let configEl = crel('div',{
'id':'configuration'
},
crel(
'div',
{
'class':'btn btn-close',
'on': {
'click': function() {
configEl.parentNode.removeChild(configEl)
}
}
},
'close'
),
crel('h2', `Language based settings for ${this.language_code}`),
crel(
'label',
'volume',
crel('input', {
'type': 'number',
'step': 0.05,
'on': {
'change': function(e){
panopticon.graph.configuration['volume'] = parseFloat(e.target.value)
}
},
'value': this.configuration.hasOwnProperty('volume') ? this.configuration.volume : 1
})
)
);
document.getElementById("interface").appendChild(configEl);
}
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';
}
// chapter marker:
let chapterAttributes = {
'name': msg['@id'] + '-chapterStart',
// 'readonly': 'readonly',
'type': 'checkbox',
'on': {
'change': this.getEditEventListener()
}
}
if ( typeof msg['chapterStart'] !== 'undefined' && msg['chapterStart'] == true ) {
chapterAttributes['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)});
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', 'Chapter start' ),
crel( 'input', chapterAttributes )
),
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) {
msgEl.appendChild(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'], true);
});
}
}
},
"Save & play on #" + panopticon.hugveys.selectedId
)
));
msgEl.appendChild(crel(
'div',
{'class': 'play'},
crel(
'div', {
'class': 'btn btn--play',
'on': {
'click': function (e) {
panopticon.playFromSelected(msg['@id'], false);
}
}
},
"Continue on #" + panopticon.hugveys.selectedId
)
));
}
// 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." },
'needsReply': { 'type': 'checkbox', label: "Reply needed", "title": "If checked, the timeout is counted if met. Used by consecutive-timeouts diversions." },
},
'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' },
},
'variable': {
'variable': { 'value': '','placeholder': "Variable name (without $)" },
'notSet': { "label": "Not set", 'value': '', 'title': "Match if the variable is _not_ set.", 'type':'checkbox' },
},
'diversion': {
'diversionId': { 'tag': 'select', 'value': '','placeholder': "Variable name (without $)", 'options': this.diversions.map((d) => d['@id']) },
'inverseMatch': { "label": "Match if not done", 'value': '', 'title': "Match if the diversion has _not_ been done.", 'type':'checkbox' },
}
};
}
getConditionInputsForType( type, conditionId, values ) {
let inputs = [];
let vars = this.getConditionTypes()[type];
for ( let v in vars ) {
let attr = vars[v];
let inputType = attr.hasOwnProperty('tag') ? attr['tag'] : 'input';
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( inputType, 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(skipRebuild) {
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 );
console.log("skip or not to skip?", skipRebuild);
if(typeof skipRebuild == 'undefined' || !skipRebuild) {
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 );
let skipDistances;
// orphaned target and source has no other destinations. We can copy the vertical position:
if(this.getDirectionsFrom( source ).length < 1 && this.getDirectionsFrom( target ).length < 1 && this.getDirectionsTo( target ).length < 1) {
skipDistances = true;
let distance = this.distances[source['@id']];
let d = [distance[0] + 1, distance[1]];
// create a distance based on source's position
// this saves us from running the slow calculateDistancesFromStart
this.distances[target['@id']] = d;
} else {
skipDistances = false;
}
this.updateFromData(skipDistances);
this.build();
return dir;
}
rmDirection( dir ) {
this._rmNode( dir );
// todo, remove orphaned conditions
}
createMsg() {
this.addMsg();
this.build();
}
createConnectedMsg(sourceMsg) {
console.time('createConnected');
console.time("Add");
let newMsg = this.addMsg(true); // skipRebuild = true, as addDirection() already rebuilds the graph
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
}
console.timeEnd("Add");
console.time("direction");
this.addDirection(sourceMsg, newMsg);
console.timeEnd("direction");
console.time("build");
// this.build(); // build is already done in addDirection()
console.timeEnd("build");
// reselect so that overview is updated
console.time("Select");
this.selectMsg(newMsg);
console.timeEnd("Select");
console.timeEnd('createConnected');
}
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.target.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.target.value
if(e.target.type == 'checkbox') {
value = e.target.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.configuration, ...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(skipDistances) {
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' );
let configurations = this.data.filter(( node ) => node['@type'] == 'Configuration' );
this.configuration = configurations.length > 0 ? configurations[0] : {
"@id": "config",
"@type": "Configuration"
};
document.getElementById('current_lang').innerHTML = "";
document.getElementById('current_lang').appendChild(crel('span', {
'class': 'flag-icon ' + this.language_code
}));
let storyEl = document.getElementById('story');
storyEl.classList.remove(... panopticon.languages.map((l) => l['code']))
storyEl.classList.add(this.language_code);
if(typeof skipDistances == 'undefined' || !skipDistances) {
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;
}
if(hv['history'].hasOwnProperty('messages')){
for(let msg of hv['history']['messages']) {
document.getElementById(msg[0]['id']).classList.add('beenHit');
}
}
if(hv['history'].hasOwnProperty('directions')){
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 ( msg.hasOwnProperty('chapterStart') && msg['chapterStart'] == true ) classes.push( 'chapterStartMsg' );
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() {
console.time('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){
continue;
}
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;
console.time('step1');
for(let startMsg of starts) {
console.time('start: '+startMsg['@id']);
if(distances[startMsg['@id']] === null) {
distances[startMsg['@id']] = [0, yPos];
}
yPos = traverseMsg(startMsg['@id'], 1 , true, yPos);
yPos += 1;
console.timeEnd('start: '+startMsg['@id']);
}
console.timeEnd('step1');
console.time('step2');
// now we have the formal tree, lets try to polish the rest:
for(let msgId in distances) {
console.time('polish: '+ msgId);
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])
console.timeEnd('polish: '+ msgId);
}
console.timeEnd('step2');
// 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);
//
// }
console.timeEnd("calculateDistancesFromStart");
return distances;
}
}
//
//