hugvey/www/js/hugvey_console.js

2855 lines
99 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,
loop_timer: 0,
languages: [],
hugveys: [],
selectedId: null,
logbook: "",
logbookId: null,
selectedLang: null,
blockedHugveys: 0,
availableHugveys: 0,
starts_since_lang_change: 0,
future_language: 0,
},
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.selectHugvey(null);
if(panopticon.hasGraph) {
return panopticon.loadNarrative( code, file );
}
},
changeLanguageForAvailable: function(code){
let el = document.getElementById("change-lang-"+code);
el.classList.add('loading');
console.log(el);
panopticon.send( { action: 'change_language_for_available', lang_code: code } );
// TODO: loop with a timer
let i = 0;
for(let hv of panopticon.hugveys.hugveys) {
setTimeout(function(){
console.log(hv['available']);
if(hv.hasOwnProperty('available') && hv['available']) {
panopticon.change_language(hv.id, code);
}
}, i * 1000);
i++;
}
setTimeout(function(){el.classList.remove('loading');}, i*1000);
},
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);
},
change_light_status: function(e) {
let hv_id = parseInt(e.target.dataset.hvid);
let checked = e.target.checked;
panopticon.send( { action: 'change_light_status', hugvey: hv_id, light_status: checked } );
},
showHugvey: function(hv) {
panopticon.selectHugvey(hv.language ? hv.id : null);
panopticon.hugveys.logbook = [];
panopticon.hugveys.logbookId = null;
panopticon.updateSelectedHugvey();
},
unblockAll: function(){
let el = document.getElementById('unblock-all');
el.classList.add('loading');
let i = 0;
for(let hv of panopticon.hugveys.hugveys) {
setTimeout(function(){
if(hv.status == 'blocked') {
hv.status = "loading";
panopticon.unblock(hv.id)
}
}, i * 1000);
i++;
}
setTimeout(() => el.classList.remove('loading'), i * 1000);
},
startAll: function(){
let el = document.getElementById('start-all');
el.classList.add('loading');
let i = 0;
for(let hv of panopticon.hugveys.hugveys) {
setTimeout(function(){
if(hv.status == 'available') {
hv.status = "loading";
panopticon.restart(hv.id)
}
}, i * 1000);
i++;
}
setTimeout(() => el.classList.remove('loading'), i * 1000);
},
}
} );
// this.socket = new ReconnectingWebSocket( "ws://localhost:8888/ws", null, { debug: false, reconnectInterval: 3000 } );
this.socket = new ReconnectingWebSocket( window.location.origin.replace('http', 'ws') +'/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.loop_timer = this.stringToHHMMSS( msg['loop_timer'] );
this.hugveys.languages = msg['languages'];
this.languages = msg['languages'];
this.hugveys.hugveys = msg['hugveys'];
this.hugveys.logbook = msg['logbook'];
this.hugveys.logbookId = msg['logbookId'];
this.hugveys.starts_since_lang_change = msg['starts_since_lang_change'];
this.hugveys.future_language = msg['future_language'];
if(this.hugveys.selectedId) {
this.updateSelectedHugvey();
}
let avail = 0;
let blocked = 0;
for(let hv of this.hugveys.hugveys) {
if(hv.status =='available') avail ++;
if(hv.status =='blocked') blocked ++;
}
this.hugveys.blockedHugveys = blocked;
this.hugveys.availableHugveys = avail;
break;
case 'log':
break;
}
} );
}
selectHugvey(hv_id) {
this.hugveys.selectedId = hv_id;
this.send({ action: 'selection', selected_id: hv_id });
}
change_loop_time(newTime) {
console.log('update', newTime);
this.send({ action: 'loop_time', time: newTime });
}
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'];
}
}
}
this.hugveys.selectedLang = code;
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
this.previousSearch = "";
this.searchStep = 0;
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() {
console.log(d3.event);
c.attr( "transform", d3.event.transform );
}
this.zoom = d3.zoom();
this.svg.call( this.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 ) {
let el = e.target;
el.classList.add('loading');
// give the ui a fraction to actually apply the 'loading' class
setTimeout(function(){
graph.saveJson();
el.classList.remove('loading');
}, 100);
} );
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(); } );
document.getElementById( 'search' ).addEventListener( 'submit', function( e ) { e.preventDefault(); graph.searchSubmit(); } );
}
searchSubmit() {
let query = document.getElementById( 'search-field' ).value;
if(query.length < 1) {
console.log('no query');
return;
}
if(query == this.previousSearch) {
this.searchStep++;
} else{
this.searchStep = 0;
}
this.previousSearch = query;
let results = this.findMessages(query);
if(results.length < 1) {
alert("No message found");
return;
}
let ri = this.searchStep % results.length;
let msg = results[ri];
console.log(msg);
this.zoom.scaleTo(this.svg, 1);
this.zoom.translateTo(this.svg, msg.x + 1280/2, msg.y + 1024/2);
this.selectMsg(msg);
document.getElementById("search-go").value = "Search ("+(ri+1) + "/" + results.length +")";
}
findMessages(query) {
let results = [];
for(let msg of this.messages) {
if(msg['@id'] == query || msg['@id'].indexOf(query) > -1) {
results.push(msg);
}
if(msg['text'] == query || (msg.hasOwnProperty('label') && msg['label'] == query)) {
results.push(msg);
}
}
// second loop for priority
for(let msg of this.messages) {
if(msg['text'].indexOf(query) > -1 || (msg.hasOwnProperty('label') && msg['label'].indexOf(query) > -1)) {
results.push(msg);
}
}
return results;
}
findMessage(query, matchNr) {
if(typeof matchNr == 'undefined') {
matchNr = 0;
}
let results = this.findMessages(query)
if(results.length) {
return results[matchNr % results.length];
}
}
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 `${window.location.origin}/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']['nextChapterOnReturn'] = false;
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 if(type == 'collective_moment') {
div['params']['start_second'] = 20 * 60; // second to start
div['params']['window'] = 60; // how long to wait, in seconds
}
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 = [], divsCollectiveMoment = [];
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 , `${this.getLabel(startMsg)} (${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 returnChapterAttrs = {
'type': 'checkbox',
'on': {
'change': (e) => div['params']['nextChapterOnReturn'] = e.target.checked
}
}
if(div['params']['nextChapterOnReturn']) {
returnChapterAttrs['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', 'On return, skip to next chapter',
crel('input', returnChapterAttrs)
),
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'] == 'collective_moment') {
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']));
}
divsCollectiveMoment.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', 'Start time (seconds)',
crel('input', {
'type': 'number',
// 'max': 59,
'min': 0,
'value': div['params']['start_second'],
'placeholder': 'time in loop (seconds)',
'on': {
'change': (e) => div['params']['start_second'] = e.target.value
}
})
),
crel('label', 'Duration (seconds)',
crel('input', {
'type': 'number',
'max': 60*15,
'min': 0,
'value': div['params']['window'],
'placeholder': 'seconds',
'on': {
'change': (e) => div['params']['window'] = e.target.value
}
})
),
crel('label', 'Go to (start message)',
crel('select', {'on': {
'change': (e) => div['params']['msgId'] = e.target.value
}}, ...msgOptions)
)
));
}
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, divsCollectiveMoment);
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', 'Collective moments'),
...divsCollectiveMoment,
crel('div',
{
'class': 'btn',
'on': {
'click': (e) => this.createDiversion('collective_moment')
}
},
'New collective moment'
)
)
// ,
// 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
})
),crel('br'),
crel(
'label',
"Text replacement when no variable is set: ",
crel('input', {
'type': 'text',
'on': {
'change': function(e){
panopticon.graph.configuration['nothing_text'] = e.target.value
}
},
'value': this.configuration.hasOwnProperty('nothing_text') ? this.configuration.nothing_text : "nothing"
})
),crel('br'),
crel(
'label',
"Condition timing factor: (< 1 is faster, >1 is slower)",
crel('input', {
'type': 'number',
'on': {
'change': function(e){
panopticon.graph.configuration['time_factor'] = parseFloat(e.target.value)
}
},
'value': this.configuration.hasOwnProperty('time_factor') ? this.configuration.time_factor : 1,
'step': 0.01
})
),crel('br'),
crel(
'label',{
'title': "Since Moscow: apparently the interval at which Google returns interim results differs per language, or we have anther cause of irregular results, either way, this screws up the short waitTimes that are crucial for the replyContains condition/diversion. Therefore, have a story configuration option with which we can extra delay to the timings (if non-zero)"
},
"ReplyContains timing delay:",
crel('input', {
'type': 'number',
'on': {
'change': function(e){
panopticon.graph.configuration['time_extra_delay'] = parseFloat(e.target.value)
}
},
'value': this.configuration.hasOwnProperty('time_extra_delay') ? this.configuration.time_extra_delay : 0,
'step': 0.01
})
),'s',crel('br'),
crel(
'label',
"Playback tempo factor: (< 1 is slower, >1 is faster)",
crel('input', {
'type': 'number',
'on': {
'change': function(e){
panopticon.graph.configuration['tempo_factor'] = parseFloat(e.target.value)
}
},
'value': this.configuration.hasOwnProperty('tempo_factor') ? this.configuration.tempo_factor : 1,
'step': 0.01
})
),crel('br'),
crel(
'label',
"Playback pitch modifier: (< 0 is lower, >0 is higher)",
crel('input', {
'type': 'number',
'on': {
'change': function(e){
panopticon.graph.configuration['pitch_modifier'] = parseFloat(e.target.value)
}
},
'value': this.configuration.hasOwnProperty('pitch_modifier') ? this.configuration.pitch_modifier : 0,
'step': 1
})
),
crel('hr'),
crel('h2', 'Light fade setting #0'),
crel(
'label',
"Light intensity: ",
crel('input', {
'type': 'number',
'min': 0,
'max': 255,
'on': {
'change': function(e){
panopticon.graph.configuration['light0_intensity'] = e.target.value
}
},
'value': this.configuration.hasOwnProperty('light0_intensity') ? this.configuration.light0_intensity : ""
})
),
crel(
'label',
"Fade time: ",
crel('input', {
'type': 'number',
'min': 1,
'max': 92,
'step': 1,
'on': {
'change': function(e){
panopticon.graph.configuration['light0_fade'] = e.target.value
}
},
'value': this.configuration.hasOwnProperty('light0_fade') ? this.configuration.light0_fade: ""
})
),
crel(
'label',
"Is Sophie: ",
crel('input', {
'type': 'checkbox',
'checked_value': this.configuration.hasOwnProperty('light0_isSophie') ? this.configuration.light0_isSophie: false,
'on': {
'change': function(e){
panopticon.graph.configuration['light0_isSophie'] = e.target.checked
}
}
})
),
crel('h2', 'Light fade setting #1'),
crel(
'label',
"Light intensity: ",
crel('input', {
'type': 'number',
'min': 0,
'max': 255,
'on': {
'change': function(e){
panopticon.graph.configuration['light1_intensity'] = e.target.value
}
},
'value': this.configuration.hasOwnProperty('light1_intensity') ? this.configuration.light1_intensity : ""
})
),
crel(
'label',
"Fade time: ",
crel('input', {
'type': 'number',
'min': 1,
// 'max': 5,
'step': .1,
'on': {
'change': function(e){
panopticon.graph.configuration['light1_fade'] = e.target.value
}
},
'value': this.configuration.hasOwnProperty('light1_fade') ? this.configuration.light1_fade: ""
})
),
crel(
'label',
"Is Sophie: ",
crel('input', {
'type': 'checkbox',
'checked_value': this.configuration.hasOwnProperty('light1_isSophie') ? this.configuration.light1_isSophie: false,
'on': {
'change': function(e){
panopticon.graph.configuration['light1_isSophie'] = e.target.checked
}
}
})
),
crel('h2', 'Light fade setting #2'),
crel(
'label',
"Light intensity: ",
crel('input', {
'type': 'number',
'min': 0,
'max': 255,
'on': {
'change': function(e){
panopticon.graph.configuration['light2_intensity'] = e.target.value
}
},
'value': this.configuration.hasOwnProperty('light2_intensity') ? this.configuration.light2_intensity : ""
})
),
crel(
'label',
"Fade time: ",
crel('input', {
'type': 'number',
'min': 1,
// 'max': 5,
'step': .1,
'on': {
'change': function(e){
panopticon.graph.configuration['light2_fade'] = e.target.value
}
},
'value': this.configuration.hasOwnProperty('light2_fade') ? this.configuration.light2_fade: ""
})
),
crel(
'label',
"Is Sophie: ",
crel('input', {
'type': 'checkbox',
'checked_value': this.configuration.hasOwnProperty('light2_isSophie') ? this.configuration.light2_isSophie: false,
'on': {
'change': function(e){
panopticon.graph.configuration['light2_isSophie'] = e.target.checked
}
}
})
),
crel('h2', 'Light fade setting #3'),
crel(
'label',
"Light intensity: ",
crel('input', {
'type': 'number',
'min': 0,
'max': 255,
'on': {
'change': function(e){
panopticon.graph.configuration['light3_intensity'] = e.target.value
}
},
'value': this.configuration.hasOwnProperty('light3_intensity') ? this.configuration.light3_intensity : ""
})
),
crel(
'label',
"Fade time: ",
crel('input', {
'type': 'number',
'min': 1,
// 'max': 5,
'step': .1,
'on': {
'change': function(e){
panopticon.graph.configuration['light3_fade'] = e.target.value
}
},
'value': this.configuration.hasOwnProperty('light3_fade') ? this.configuration.light3_fade: ""
})
),
crel(
'label',
"Is Sophie: ",
crel('input', {
'type': 'checkbox',
'checked_value': this.configuration.hasOwnProperty('light3_isSophie') ? this.configuration.light3_isSophie: false,
'on': {
'change': function(e){
panopticon.graph.configuration['light3_isSophie'] = e.target.checked
}
}
})
),
crel('h2', 'Light fade setting #4'),
crel(
'label',
"Light intensity: ",
crel('input', {
'type': 'number',
'min': 0,
'max': 255,
'on': {
'change': function(e){
panopticon.graph.configuration['light4_intensity'] = e.target.value
}
},
'value': this.configuration.hasOwnProperty('light4_intensity') ? this.configuration.light4_intensity : ""
})
),
crel(
'label',
"Fade time: ",
crel('input', {
'type': 'number',
'min': 1,
// 'max': 5,
'step': .1,
'on': {
'change': function(e){
panopticon.graph.configuration['light4_fade'] = e.target.value
}
},
'value': this.configuration.hasOwnProperty('light4_fade') ? this.configuration.light4_fade: ""
})
),
crel(
'label',
"Is Sophie: ",
crel('input', {
'type': 'checkbox',
'checked_value': this.configuration.hasOwnProperty('light4_isSophie') ? this.configuration.light4_isSophie: false,
'on': {
'change': function(e){
panopticon.graph.configuration['light4_isSophie'] = e.target.checked
}
}
})
)
);
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 generatedDirectionsJumpToChapterAttributes = {
'name': msg['@id'] + '-generatedDirectionsJumpToChapter',
// 'readonly': 'readonly',
'type': 'checkbox',
'on': {
'change': this.getEditEventListener()
}
}
if ( msg.hasOwnProperty('generatedDirectionsJumpToChapter') && msg['generatedDirectionsJumpToChapter'] == true ) {
generatedDirectionsJumpToChapterAttributes['checked'] = 'checked';
}
let dontGenerateDirectionsAttributes = {
'name': msg['@id'] + '-dontGenerateDirections',
// 'readonly': 'readonly',
'type': 'checkbox',
'on': {
'change': this.getEditEventListener()
}
}
if ( msg.hasOwnProperty('dontGenerateDirections') && msg['dontGenerateDirections'] == true ) {
dontGenerateDirectionsAttributes['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 lightOptions = [
crel("option", {'value': null}, "Do nothing")
];
for(let i = 0; i < 5; i++) {
let l = {'value': i};
if(msg.hasOwnProperty('light') && msg['light'] == i) {
l['selected'] = 'selected';
}
let intensity = "?";
if(this.configuration.hasOwnProperty(`light${i}_intensity`)) {
intensity = this.configuration[`light${i}_intensity`];
}
let duration = "?";
if(this.configuration.hasOwnProperty(`light${i}_fade`)) {
duration = this.configuration[`light${i}_fade`];
}
lightOptions.push(crel("option", l, `Fade preset #${i} (${intensity} in ${duration}s)`));
}
// let lightOptionNone = {'value': null}
//
// let lightOptionOn = {'value': 1}
// let lightOptionOff = {'value': 0}
//
// if(msg.hasOwnProperty('light')) {
// if(msg['light'] === 1) lightOptionOn['selected'] = 'selected';
// if(msg['light'] === 0) lightOptionOff['selected'] = 'selected';
// if(msg['light'] === null) lightOptionNone['selected'] = 'selected';
// }
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', 'Label' ),
crel( 'input', {
'name': msg['@id'] + '-label',
'value': msg.hasOwnProperty('label') ? msg['label'] : "",
'on': {
'change': this.getEditEventListener()
}
} )
),
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'] : .8,
'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: negative: lower, positive: higher"
}, 'Pitch' ),
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()
}
} )
),
crel( 'label',
crel( 'span', {
"title": "What to do with the light when this message is triggered?"
}, 'Light change' ),
crel( 'select', {
'name': msg['@id'] + '-light',
'on': {
'change': function(e) {
msg['light'] = e.target.value === "null" ? null : parseInt(e.target.value);
panopticon.graph.build();
}
}
},
lightOptions
)
),
crel( 'label',
crel( 'span', {
'title': "Only for diversions: when this message is the final message of a diversion, it jumps back to the next chapter instead of the given message (overrules settings of diversion on a per message basis)"
},'Generated directions jump to next chapter' ),
crel( 'input', generatedDirectionsJumpToChapterAttributes )
),
crel( 'label',
crel( 'span', {
'title': "Only for diversions: when this message is the final message of a diversion, it ends the story instead of generating a return direction"
},'Is story ending (when diversion)' ),
crel( 'input', dontGenerateDirectionsAttributes )
),
);
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 );
}
getLabel(msg) {
if(msg.hasOwnProperty('label') && msg['label'].length > 0)
return msg['label'];
return msg['text'];
}
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 ${this.getLabel(direction['target'])}`: `From ${this.getLabel(direction['source'])}`
),
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." },
},
'variable_storage': {
// when matched, variable will be accessible as {store_name_1}
'var_name': { 'label': "Variable name", 'type': 'text', 'description': "When matched, variable will be accessible as $stored_VARNAME_1, $stored_VARNAME_2.. etc (use the name given here instead of VARNAME)" },
'number': { 'label': "Nr. of items to get", 'type': 'number', 'value': 5, 'min': 0, 'step': 1 },
'unique': { 'label': "Unique items", 'type': 'checkbox', 'title': "If checked, every word is returned only once, eg. love, dream, love, returns love, dream"},
},
'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' },
},
'loop_time': {
// can be used in two ways
'less_than': {'type':'number', 'value': 0, 'min':0, 'step': 1, 'label': "Time in seconds, the loop should be before, 0 is ignored"},
'more_than': {'type':'number', 'value': 0, 'min':0, 'step': 1, 'label': "Time the loop should be after, 0 is ignored"},
'inverseMatch': { 'title': "Inverse the matching.", 'type':'checkbox' },
},
'variable': {
'variable': { 'value': '','placeholder': "Variable name (without $)" },
'notSet': { "label": "Not set", 'value': '', 'title': "Match if the variable is _not_ set.", 'type':'checkbox' },
},
'variableEquals': {
'variable1': { 'value': '','placeholder': "Variable name (without $)" },
'variable2': { 'value': '','placeholder': "Variable name (without $)" },
'notEq': { "label": "Not equal", 'value': '', 'title': "Match if the variables are _not_ equal.", 'type':'checkbox' },
},
'diversion': {
'diversionId': { 'tag': 'select', 'value': '', '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' },
},
'messagePlayed': {
'msgId': { 'value': '', 'label':'Message ID', 'placeholder': "(eg. en-njsm9b0ni)" },
'inverseMatch': { "label": "Match if not played", 'value': '', 'title': "Match if the message has _not_ been played.", 'type':'checkbox' },
},
// audioError has no parameters. Just checks if there was an error fetching the audio file
'audioError': {}
};
}
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'] : "" )
)
);
if(attr.hasOwnProperty('description')) {
inputs.push(crel('div', {'class':'description'}, attr['description']));
}
}
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,
"light": null,
}
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']];
if(distance == null) {
skipDistances = false;
} else {
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(); // already happens in addMsg()
}
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', 'x', 'y', 'vx', 'vy']
for ( let node of this.data ) {
let n = {};
// console.log( node['source'] );
for ( let e in node ) {
if ( node.hasOwnProperty( e ) && toRemove.indexOf( e ) == -1 ) {
if ( this.data.indexOf( node[e] ) != -1 ) {
n[e] = node[e]['@id'];
} else {
n[e] = node[e];
}
}
}
d.push( n );
}
console.info("Jsonified graph:",d);
return JSON.stringify( d );
}
downloadJson() {
if ( !this.language_code ) {
alert( "Make sure to load a language first" )
}
var blob = new Blob( [this.getJsonString()], { type: 'application/json' } );
if ( window.navigator.msSaveOrOpenBlob ) {
window.navigator.msSaveBlob( blob, "pillow_talk.json" );
}
else {
var elem = window.document.createElement( 'a' );
elem.href = window.URL.createObjectURL( blob );
elem.download = "pillow_talk.json";
document.body.appendChild( elem );
elem.click();
document.body.removeChild( elem );
}
}
saveJson( msg_id, fileInputElement, callback ) {
if ( !this.language_code ) {
alert( "Make sure to load a language first" )
}
let formData = new FormData();
formData.append( "language", this.language_code );
if ( msg_id ) {
formData.append( "message_id", msg_id );
formData.append( "audio", fileInputElement.files[0] );
}
let blob = new Blob( [this.getJsonString()], { type: "application/json" } );
formData.append( "json", blob );
console.info("Save json", formData );
var request = new XMLHttpRequest();
request.open( "POST", window.location.origin + "/upload" );
if(callback) {
request.addEventListener( "load", callback);
}
request.send( formData );
}
loadData( data, language_code ) {
console.time('load');
this.language_code = language_code;
this.data = data;
console.time('load:update');
this.updateFromData();
console.timeEnd('load:update');
console.time('load:build');
this.build( true );
console.timeEnd('load:build');
console.timeEnd('load');
}
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;
// console.time('update:save')
// this.saveState();
// console.timeEnd('update:save')
}
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 ) {
console.trace();
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( `${this.getLabel(d)}` ) );
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();
console.time('build:simulate')
for ( let i = 0, n = Math.ceil( Math.log( this.simulation.alphaMin() ) / Math.log( 1 - this.simulation.alphaDecay() ) ); i < n; ++i ) {
this.simulation.tick();
}
console.timeEnd('build:simulate')
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;
}
}
//
//