hugvey/www/js/hugvey_console.js

1962 lines
69 KiB
JavaScript
Raw Normal View History

2019-01-23 14:26:44 +00:00
var panopticon;
class Panopticon {
constructor() {
console.log( "Init panopticon" );
this.languages = []
2019-01-25 14:45:46 +00:00
// this.selectedHugvey = null;
2019-01-23 14:26:44 +00:00
this.hugveys = new Vue( {
2019-01-25 14:45:46 +00:00
el: "#interface",
2019-01-23 14:26:44 +00:00
data: {
uptime: 0,
languages: [],
2019-01-25 14:45:46 +00:00
hugveys: [],
selectedId: null,
2019-01-23 14:26:44 +00:00
},
methods: {
2019-01-24 13:27:04 +00:00
time_passed: function( hugvey, property ) {
return moment( Date( hugvey[property] * 1000 ) ).fromNow();
2019-01-23 14:26:44 +00:00
},
timer: function(hugvey, property) {
return panopticon.stringToHHMMSS( hugvey[property] );
},
2019-01-24 13:27:04 +00:00
loadNarrative: function( code, file ) {
2019-01-25 14:45:46 +00:00
panopticon.hugveys.selectedId = null;
2019-01-24 13:27:04 +00:00
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);
},
2019-01-25 10:59:03 +00:00
pause: function(hv) {
hv.status = "loading";
return panopticon.pause(hv.id);
},
2019-01-25 10:59:03 +00:00
resume: function(hv) {
hv.status = "loading";
return panopticon.resume(hv.id);
},
2019-01-25 10:59:03 +00:00
restart: function(hv) {
hv.status = "loading";
return panopticon.restart(hv.id);
},
finish: function(hv) {
hv.status = "loading";
return panopticon.finish(hv.id);
},
2019-01-25 10:59:03 +00:00
change_lang: function(hv, lang_code) {
hv.status = "loading";
return panopticon.change_language(hv.id, lang_code);
2019-01-25 13:10:19 +00:00
},
showHugvey: function(hv) {
2019-01-25 14:45:46 +00:00
panopticon.hugveys.selectedId = hv.language ? hv.id : null;
2019-01-25 13:10:19 +00:00
panopticon.updateSelectedHugvey();
2019-01-23 14:26:44 +00:00
}
}
} );
2019-01-24 13:27:04 +00:00
this.socket = new ReconnectingWebSocket( "ws://localhost:8888/ws", null, { debug: false, reconnectInterval: 3000 } );
2019-01-23 14:26:44 +00:00
this.graph = new Graph();
2019-01-23 14:26:44 +00:00
this.socket.addEventListener( 'open', ( e ) => {
this.send( { action: 'init' } );
} );
// request close before unloading
window.addEventListener('beforeunload', function(){
panopticon.socket.close();
});
2019-01-23 14:26:44 +00:00
this.socket.addEventListener( 'close', function( e ) {
console.log( 'Closed connection' );
} );
this.socket.addEventListener( 'message', ( e ) => {
let msg = JSON.parse( e.data );
if ( typeof msg['alert'] !== 'undefined' ) {
2019-01-24 13:27:04 +00:00
alert( msg['alert'] );
2019-01-23 14:26:44 +00:00
}
2019-01-24 13:27:04 +00:00
2019-01-23 14:26:44 +00:00
if ( typeof msg['action'] === 'undefined' ) {
console.error( "not a valid message: " + e.data );
return;
}
console.debug(msg);
2019-01-23 14:26:44 +00:00
switch ( msg['action'] ) {
2019-01-24 13:27:04 +00:00
2019-01-23 14:26:44 +00:00
case 'status':
2019-01-24 13:27:04 +00:00
this.hugveys.uptime = this.stringToHHMMSS( msg['uptime'] );
2019-01-23 14:26:44 +00:00
this.hugveys.languages = msg['languages'];
this.languages = msg['languages'];
2019-01-23 14:26:44 +00:00
this.hugveys.hugveys = msg['hugveys'];
2019-01-25 14:45:46 +00:00
if(this.hugveys.selectedId) {
2019-01-25 13:10:19 +00:00
this.updateSelectedHugvey();
}
2019-01-23 14:26:44 +00:00
break;
case 'log':
break;
2019-01-23 14:26:44 +00:00
}
} );
}
2019-01-25 13:10:19 +00:00
updateSelectedHugvey() {
2019-01-25 14:45:46 +00:00
let hv = null;
2019-01-25 14:45:46 +00:00
if(this.hugveys.selectedId) {
hv = this.getHugvey(this.hugveys.selectedId);
if(hv.language && this.graph.language_code != hv.language) {
this.loadNarrative(hv.language);
}
2019-01-25 13:10:19 +00:00
}
this.graph.updateHugveyStatus(hv);
}
getHugvey(id) {
for(let hv of this.hugveys.hugveys) {
if(hv.id == id) {
return hv;
}
}
return null;
}
2019-01-23 14:26:44 +00:00
send( msg ) {
2019-01-24 13:27:04 +00:00
if ( this.socket.readyState == WebSocket.OPEN ) {
this.socket.send( JSON.stringify( msg ) );
2019-01-23 14:26:44 +00:00
} else {
2019-01-24 13:27:04 +00:00
console.error( "Socket not open: ", this.socket.readyState );
2019-01-23 14:26:44 +00:00
}
}
getStatus() {
2019-01-24 13:27:04 +00:00
// console.log('get status', this, panopticon);
panopticon.send( { action: 'get_status', selected_id: panopticon.hugveys.selectedId } );
2019-01-23 14:26:44 +00:00
}
init() {
setInterval( this.getStatus, 3000 );
}
2019-01-24 13:27:04 +00:00
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'];
}
}
}
2019-01-23 14:26:44 +00:00
let req = new XMLHttpRequest();
let graph = this.graph;
2019-01-24 13:27:04 +00:00
req.addEventListener( "load", function( e ) {
graph.loadData( JSON.parse( this.response ), code );
// console.log(, e);
} );
req.open( "GET", "/local/" + file );
2019-01-23 14:26:44 +00:00
req.send();
}
block( hv_id ) {
this.send( { action: 'block', hugvey: hv_id } )
}
unblock( hv_id ) {
this.send( { action: 'unblock', hugvey: hv_id } )
}
2019-01-24 13:27:04 +00:00
resume( hv_id ) {
this.send( { action: 'resume', hugvey: hv_id } )
2019-01-23 14:26:44 +00:00
}
2019-01-24 13:27:04 +00:00
pause( hv_id ) {
2019-01-25 13:10:19 +00:00
this.send( { action: 'pause', hugvey: hv_id } )
2019-01-23 14:26:44 +00:00
}
2019-01-24 13:27:04 +00:00
restart( hv_id ) {
2019-01-25 13:10:19 +00:00
this.send( { action: 'restart', hugvey: hv_id } );
2019-01-23 14:26:44 +00:00
}
finish( hv_id ) {
this.send( { action: 'finish', hugvey: hv_id } );
}
2019-01-25 10:59:03 +00:00
change_language( hv_id, lang_code ) {
this.send( { action: 'change_language', hugvey: hv_id, lang_code: lang_code } );
}
2019-01-25 14:45:46 +00:00
playFromSelected(msg_id) {
if(!this.hugveys.selectedId) {
alert('No hugvey selected');
} else {
this.send({ action: 'play_msg', hugvey: this.hugveys.selectedId, msg_id: msg_id })
}
}
2019-01-23 14:26:44 +00:00
}
window.addEventListener( 'load', function() {
panopticon = new Panopticon();
panopticon.init();
2019-01-24 13:27:04 +00:00
} );
2019-01-23 14:26:44 +00:00
2019-01-24 13:27:04 +00:00
class Graph {
2019-01-23 14:26:44 +00:00
constructor() {
this.width = 1280;
this.height = 1024;
this.nodeSize = 80;
this.maxChars = 16;
2019-01-24 13:27:04 +00:00
this.svg = d3.select( '#graph' );
this.container = d3.select( '#container' );
2019-01-23 14:26:44 +00:00
this.selectedMsg = null;
2019-01-23 21:38:27 +00:00
this.language_code = null;
2019-01-23 14:26:44 +00:00
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
2019-01-23 14:26:44 +00:00
let graph = this;
this.controlDown = false;
2019-01-24 13:27:04 +00:00
document.addEventListener( 'keydown', function( e ) {
2019-02-02 17:20:55 +00:00
if ( e.which == "16" ) { // shift
2019-01-23 14:26:44 +00:00
graph.controlDown = true;
2019-01-24 13:27:04 +00:00
document.body.classList.add( 'controlDown' );
2019-01-23 14:26:44 +00:00
}
2019-01-24 13:27:04 +00:00
} );
document.addEventListener( 'keyup', function( e ) {
2019-02-02 17:20:55 +00:00
if ( e.which == "16" ) { // shift
2019-01-23 14:26:44 +00:00
graph.controlDown = false;
2019-01-24 13:27:04 +00:00
document.body.classList.remove( 'controlDown' );
2019-01-23 14:26:44 +00:00
}
2019-01-24 13:27:04 +00:00
} );
2019-01-23 14:26:44 +00:00
let c = this.container;
2019-01-24 13:27:04 +00:00
let zoomed = function() {
c.attr( "transform", d3.event.transform );
2019-01-23 14:26:44 +00:00
}
2019-01-24 13:27:04 +00:00
this.svg.call( d3.zoom()
2019-02-25 16:05:14 +00:00
.scaleExtent( [1 / 16, 8] )
2019-01-24 13:27:04 +00:00
.on( "zoom", zoomed ) );
2019-01-23 14:26:44 +00:00
2019-01-24 13:27:04 +00:00
this.nodesG = this.container.append( "g" )
.attr( "id", "nodes" )
2019-01-23 14:26:44 +00:00
2019-01-24 13:27:04 +00:00
this.linkG = this.container.append( "g" )
.attr( "id", "links" );
2019-01-23 14:26:44 +00:00
2019-01-24 13:27:04 +00:00
document.getElementById( 'btn-save' ).addEventListener( 'click', function( e ) { graph.saveJson(); } );
document.getElementById( 'btn-addMsg' ).addEventListener( 'click', function( e ) { graph.createMsg(); } );
2019-03-07 19:19:43 +00:00
document.getElementById( 'btn-diversions' ).addEventListener( 'click', function( e ) { graph.showDiversions(); } );
2019-04-10 16:46:32 +00:00
document.getElementById( 'btn-audio' ).addEventListener( 'click', function( e ) { graph.showAudioFiles(); } );
2019-01-23 14:26:44 +00:00
}
2019-01-24 13:27:04 +00:00
clickMsg( msg ) {
2019-01-23 14:26:44 +00:00
// event when a message is clicked.
2019-01-24 13:27:04 +00:00
if ( this.controlDown ) {
this.secondarySelectMsg( msg );
2019-01-23 14:26:44 +00:00
} else {
2019-01-24 13:27:04 +00:00
this.selectMsg( msg );
2019-01-23 14:26:44 +00:00
}
}
2019-01-24 13:27:04 +00:00
secondarySelectMsg( msg ) {
if ( this.selectedMsg !== null ) {
this.addDirection( this.selectedMsg, msg );
2019-01-23 14:26:44 +00:00
} else {
2019-01-24 13:27:04 +00:00
console.error( 'No message selected as Source' );
2019-01-23 14:26:44 +00:00
}
}
2019-01-24 13:27:04 +00:00
selectMsg( msg ) {
let selectedEls = document.getElementsByClassName( 'selectedMsg' );
while ( selectedEls.length > 0 ) {
selectedEls[0].classList.remove( 'selectedMsg' );
2019-01-23 14:26:44 +00:00
}
2019-01-24 13:27:04 +00:00
document.getElementById( msg['@id'] ).classList.add( 'selectedMsg' );
2019-01-23 14:26:44 +00:00
this.selectedMsg = msg;
2019-01-24 13:27:04 +00:00
this.showMsg( msg );
2019-01-23 14:26:44 +00:00
}
updateMsg() {
// used eg. after a condition creation.
2019-01-24 13:27:04 +00:00
this.showMsg( this.selectedMsg );
2019-01-23 14:26:44 +00:00
}
2019-02-18 19:38:54 +00:00
getAudioUrlForMsg(msg) {
let isVariable = msg['text'].includes('$') ? '1' : '0';
2019-04-09 07:40:50 +00:00
let lang = panopticon.graph.language_code;
return `http://localhost:8888/voice?text=${encodeURIComponent(msg['text'])}&variable=${isVariable}&lang=${lang}&filename=0`;
2019-02-18 19:38:54 +00:00
}
2019-03-07 19:19:43 +00:00
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'] = "";
}
2019-04-24 14:09:41 +00:00
else if(type == 'timeout') {
div['params']['interval'] = 20;
div['params']['timesOccured'] = 0;
2019-04-24 14:50:34 +00:00
div['params']['minTimeAfterMessage'] = 2.;
2019-04-24 14:09:41 +00:00
div['params']['fromLastMessage'] = false;
div['params']['returnAfterStrand'] = true;
div['params']['msgId'] = "";
}
2019-03-07 19:19:43 +00:00
else if(type == 'repeat') {
div['params']['regex'] = "can you repeat that\\?";
} else {
console.log("invalid type", type);
alert('invalid type for diversion');
}
this.data.push( div );
this.updateFromData();
this.build();
this.showDiversions();
return msg;
}
deleteDiversion(div) {
this._rmNode( div );
this.showDiversions( );
}
showDiversions( ) {
let msgEl = document.getElementById( 'msg' );
msgEl.innerHTML = "";
let divsNoResponse =[], divsRepeat = [], divsReplyContains = [], divsTimeouts = [];
2019-03-07 19:19:43 +00:00
for(let div of this.diversions) {
if(div['type'] == 'no_response') {
let returnAttrs = {
'type': 'checkbox',
'on': {
'change': (e) => div['params']['returnAfterStrand'] = e.target.checked
}
}
if(div['params']['returnAfterStrand']) {
returnAttrs['checked'] = 'checked';
}
let msgOptions = [crel('option',"")];
let starts = this.messages.filter( m => m.hasOwnProperty('start') && m['start'] == true);
for(let startMsg of starts) {
let optionParams = {};
if(div['params']['msgId'] == startMsg['@id']) {
optionParams['selected'] = 'selected';
}
msgOptions.push(crel('option', optionParams , startMsg['@id']));
}
divsNoResponse.push(crel(
'div', {
'class': 'diversion',
'on': {
'mouseover': function(e) {
if(div['params']['msgId'])
document.getElementById(div['params']['msgId']).classList.add('selectedMsg');
},
'mouseout': function(e) {
if(div['params']['msgId'])
document.getElementById(div['params']['msgId']).classList.remove('selectedMsg');
}
}
},
crel('h3', div['@id']),
crel(
'div', {
'class':'btn btn--delete',
'on': {
'click': (e) => this.deleteDiversion(div)
}
}, 'Delete diversion'),
crel('label', 'Consecutive Silences',
crel('input', {
'type': 'number',
'value': div['params']['consecutiveSilences'],
'on': {
'change': (e) => div['params']['consecutiveSilences'] = parseInt(e.target.value)
}
})
),
crel('label', 'On n-th instance',
crel('input', {
'type': 'number',
'value': div['params']['timesOccured'],
'on': {
'change': (e) => div['params']['timesOccured'] = parseInt(e.target.value)
}
})
),
crel('label', 'Return to point of departure afterwards',
crel('input', returnAttrs)
),
crel('label', 'Go to (start message)',
crel('select', {'on': {
'change': (e) => div['params']['msgId'] = e.target.value
}}, ...msgOptions)
)
));
}
if(div['type'] == '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)
)
));
2019-03-07 19:19:43 +00:00
}
2019-04-24 14:09:41 +00:00
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)
}
})
2019-04-24 14:50:34 +00:00
),
crel('label', 'Minimum time after message',
crel('input', {
'type': 'number',
'value': div['params']['minTimeAfterMessage'],
'on': {
'change': (e) => div['params']['minTimeAfterMessage'] = parseFloat(e.target.value)
}
})
2019-04-24 14:09:41 +00:00
),
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)
)
));
}
2019-03-07 19:19:43 +00:00
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
}
})
)
));
}
}
2019-04-24 14:09:41 +00:00
console.log(divsReplyContains, divsNoResponse, divsRepeat, divsTimeouts);
2019-03-07 19:19:43 +00:00
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'
)
),
2019-03-07 19:19:43 +00:00
crel('div',
crel('h2', 'Request repeat'),
...divsRepeat,
crel('div',
{
'class': 'btn',
'on': {
'click': (e) => this.createDiversion('repeat')
}
},
'New case for repeat'
)
2019-04-24 14:09:41 +00:00
),
crel('div',
crel('h2', 'Timeouts'),
...divsTimeouts,
crel('div',
{
'class': 'btn',
'on': {
'click': (e) => this.createDiversion('timeout')
}
},
'New case for timeout'
)
2019-03-07 19:19:43 +00:00
)
);
msgEl.appendChild(divEl);
}
2019-01-23 14:26:44 +00:00
2019-04-10 16:46:32 +00:00
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);
}
2019-01-24 13:27:04 +00:00
showMsg( msg ) {
let msgEl = document.getElementById( 'msg' );
2019-01-23 14:26:44 +00:00
msgEl.innerHTML = "";
2019-01-25 14:45:46 +00:00
if(msg == null){
return;
2019-01-25 14:45:46 +00:00
}
2019-01-24 13:27:04 +00:00
let startAttributes = {
2019-01-23 14:26:44 +00:00
'name': msg['@id'] + '-start',
// 'readonly': 'readonly',
2019-01-23 14:26:44 +00:00
'type': 'checkbox',
'on': {
'change': this.getEditEventListener()
}
}
2019-01-24 13:27:04 +00:00
if ( msg['start'] == true ) {
2019-01-23 14:26:44 +00:00
startAttributes['checked'] = 'checked';
}
2019-03-07 19:19:43 +00:00
let beginningAttributes = {
'name': msg['@id'] + '-beginning',
// 'readonly': 'readonly',
'type': 'checkbox',
'on': {
'change': this.getEditEventListener()
}
}
if ( msg['beginning'] == true ) {
beginningAttributes['checked'] = 'checked';
}
2019-02-18 19:38:54 +00:00
// 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';
}
2019-02-28 17:58:03 +00:00
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)});
2019-01-24 14:01:01 +00:00
let audioSpan = crel(
'span',
{
'title': msg['audio'] ? msg['audio']['file'] : "",
'class': "label-value",
},
2019-02-18 19:38:54 +00:00
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')
2019-01-24 14:01:01 +00:00
);
2019-01-24 13:27:04 +00:00
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'),
2019-02-25 11:18:45 +00:00
crel( 'h1', { 'class': 'msg__id' }, msg['@id'] + ` (${panopticon.graph.distances[msg['@id']]})` ),
2019-01-24 13:27:04 +00:00
crel( 'label',
crel( 'span', 'Text' ),
crel( 'input', {
2019-01-23 14:26:44 +00:00
'name': msg['@id'] + '-text',
'value': msg['text'],
'on': {
2019-02-18 19:38:54 +00:00
'change': this.getEditEventListener(function(){
audioSrcEl.src = panopticon.graph.getAudioUrlForMsg(msg);
audioSrcEl.parentElement.load();
})
2019-01-23 14:26:44 +00:00
}
} )
),
2019-01-24 13:27:04 +00:00
crel( 'label',
crel( 'span', 'Start' ),
crel( 'input', startAttributes )
2019-01-24 14:01:01 +00:00
),
2019-03-07 19:19:43 +00:00
crel( 'label',
crel( 'span', 'Beginning' ),
crel( 'input', beginningAttributes )
),
crel( 'label',
crel( 'span', 'Chapter start' ),
crel( 'input', chapterAttributes )
),
2019-01-24 14:01:01 +00:00
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);
2019-01-24 14:01:01 +00:00
audioSpan.innerHTML = e.target.files[0].name + "<sup>*</sup>";
// reload graph:
// console.log('reload', panopticon.graph.language_code);
panopticon.loadNarrative(panopticon.graph.language_code);
2019-01-24 14:01:01 +00:00
});
// console.log(this,e);
}
}
} )
),
crel( 'label',
2019-02-28 17:58:03 +00:00
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()
}
} )
2019-01-23 14:26:44 +00:00
)
);
2019-01-24 13:27:04 +00:00
msgEl.appendChild( msgInfoEl );
2019-01-23 14:26:44 +00:00
if(panopticon.hugveys.selectedId) {
let playEl = crel(
'div',
{'class': 'play'},
crel(
'div', {
'class': 'btn btn--play',
'on': {
'click': function (e) {
console.log('go save');
panopticon.graph.saveJson(null, null, function(){
console.log('saved, now play');
panopticon.playFromSelected(msg['@id']);
});
}
}
},
"Save & play on #" + panopticon.hugveys.selectedId
)
);
msgEl.appendChild(playEl);
}
2019-01-23 14:26:44 +00:00
// let directionHEl = document.createElement('h2');
// directionHEl.innerHTML = "Directions";
2019-01-24 13:27:04 +00:00
let fromDirections = [], toDirections = [];
2019-01-23 14:26:44 +00:00
2019-01-24 13:27:04 +00:00
for ( let direction of this.getDirectionsTo( msg ) ) {
toDirections.push( this.getDirectionEl( direction, msg ) );
2019-01-23 14:26:44 +00:00
}
2019-01-24 13:27:04 +00:00
for ( let direction of this.getDirectionsFrom( msg ) ) {
fromDirections.push( this.getDirectionEl( direction, msg ) );
2019-01-23 14:26:44 +00:00
}
2019-01-24 13:27:04 +00:00
let directionsEl = crel( 'div', { 'class': 'directions' },
crel( 'h2', 'Directions' ),
2019-01-30 17:00:40 +00:00
...toDirections,
crel('div',{
'class': 'btn btn--create',
'on': {
'click': function(e) {
panopticon.graph.createConnectedMsg(msg);
}
}
}, 'Create new message'),
...fromDirections
2019-01-23 14:26:44 +00:00
);
2019-01-24 13:27:04 +00:00
msgEl.appendChild( directionsEl );
2019-01-23 14:26:44 +00:00
}
2019-01-24 13:27:04 +00:00
getDirectionEl( direction, msg ) {
2019-01-23 14:26:44 +00:00
let g = this;
2019-01-24 13:27:04 +00:00
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']},
2019-02-18 21:33:31 +00:00
direction['source'] == msg ? `To ${direction['target']['text']}`: `From ${direction['source']['text']}`
2019-01-24 13:27:04 +00:00
),
crel('div', {
'class':'btn btn--delete',
'on': {
'click': ( e ) => {
if(confirm("Do you want to remove this direction and its conditions?")) {
g.rmDirection( direction );
}
}
2019-01-24 13:27:04 +00:00
}
}, 'disconnect')
2019-01-24 13:27:04 +00:00
);
for ( let conditionId of direction['conditions'] ) {
let condition = this.getNodeById( conditionId );
directionEl.appendChild( this.getEditConditionFormEl( condition, direction ) );
2019-01-23 14:26:44 +00:00
}
2019-01-24 13:27:04 +00:00
directionEl.appendChild( this.getAddConditionFormEl( direction ) );
2019-01-23 14:26:44 +00:00
return directionEl;
}
2019-01-24 13:27:04 +00:00
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?")) {
2019-02-25 14:56:59 +00:00
// console.log('remove condition for direction', condition, direction);
panopticon.graph.rmCondition( condition, direction );
}
}
}
}, 'delete'),
...this.getConditionInputsForType(condition['type'], condition['@id'], condition['vars'])
2019-01-23 14:26:44 +00:00
)
2019-01-24 13:27:04 +00:00
let labelLabel = document.createElement( 'label' );
2019-01-23 14:26:44 +00:00
labelLabel.innerHTML = "Description";
2019-01-24 13:27:04 +00:00
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 );
2019-01-23 14:26:44 +00:00
// 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 );
// }
2019-01-23 14:26:44 +00:00
return conditionEl;
}
getConditionTypes() {
return {
2019-01-23 14:26:44 +00:00
'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." },
2019-04-02 06:54:26 +00:00
'needsReply': { 'type': 'checkbox', label: "Reply needed", "title": "If checked, the timeout is counted if met. Used by consecutive-timeouts diversions." },
2019-01-23 14:26:44 +00:00
},
'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' },
2019-03-29 13:11:48 +00:00
},
'variable': {
'variable': { 'value': '','placeholder': "Variable name (without $)" },
'notSet': { "label": "Not set", 'value': '', 'title': "Match if the variable is _not_ set.", 'type':'checkbox' },
2019-04-26 09:51:07 +00:00
},
'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' },
2019-01-23 14:26:44 +00:00
}
};
2019-01-23 14:26:44 +00:00
}
getConditionInputsForType( type, conditionId, values ) {
let inputs = [];
2019-01-23 14:26:44 +00:00
let vars = this.getConditionTypes()[type];
2019-01-24 13:27:04 +00:00
for ( let v in vars ) {
2019-01-23 14:26:44 +00:00
let attr = vars[v];
2019-04-26 09:51:07 +00:00
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(
2019-01-24 13:27:04 +00:00
crel( 'label',
crel( 'span', {
'title': attr.hasOwnProperty('title') ? attr['title'] : ""
}, attr.hasOwnProperty('label') ? attr['label'] : v ),
2019-04-26 09:51:07 +00:00
crel( inputType, attr )
// crel('span', {'class': 'label-unit'}, attr.hasOwnProperty('unit') ? attr['unit'] : "" )
2019-01-23 14:26:44 +00:00
)
);
}
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;
2019-01-23 14:26:44 +00:00
}
2019-01-24 13:27:04 +00:00
getAddConditionFormEl( direction ) {
2019-01-23 14:26:44 +00:00
let optionEls = [];
let types = this.getConditionTypes();
2019-01-24 13:27:04 +00:00
for ( let type in types ) {
optionEls.push( crel( 'option', type ) );
2019-01-23 14:26:44 +00:00
}
2019-01-24 13:27:04 +00:00
let conditionForm = crel( 'div', { 'class': 'condition--vars' } );
2019-01-23 14:26:44 +00:00
let g = this;
2019-01-24 13:27:04 +00:00
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];
2019-02-25 14:56:59 +00:00
// console.log(defs);
for(let field in defs) {
2019-02-25 14:56:59 +00:00
// console.log(field);
if(defs[field]['type'] == 'checkbox') {
console.info('configure checkbox', field);
form.set(field, form.has(field));
}
}
2019-01-24 13:27:04 +00:00
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);
2019-01-23 14:26:44 +00:00
}
// TODO: checkboxes
2019-02-25 14:56:59 +00:00
// console.log("Createded", vars);
2019-01-24 13:27:04 +00:00
g.addConditionForDirection( type, label, vars, direction );
2019-01-23 14:26:44 +00:00
}
2019-01-24 13:27:04 +00:00
}
},
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 );
}
2019-01-23 14:26:44 +00:00
}
2019-01-24 13:27:04 +00:00
}, optionEls ),
),
crel( "label",
crel( 'span', "Description" ),
crel( 'input', { 'name': 'label' } )
),
conditionForm,
crel( 'input', {
'type': 'submit',
'value': 'create'
} )
)
2019-01-23 14:26:44 +00:00
)
);
2019-01-24 13:27:04 +00:00
this.fillConditionFormForType( conditionForm, optionEls[0].value );
2019-01-23 14:26:44 +00:00
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 ) {
2019-01-23 14:26:44 +00:00
let id = condition['@id'];
// TODO
2019-01-24 13:27:04 +00:00
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 );
2019-01-23 14:26:44 +00:00
}
this.updateMsg();
2019-01-23 14:26:44 +00:00
}
2019-01-24 13:27:04 +00:00
getConditionEl( condition ) {
let conditionEl = document.createElement( 'div' );
2019-01-23 14:26:44 +00:00
return conditionEl;
}
2019-01-24 13:27:04 +00:00
getDirectionsFrom( msg ) {
return this.directions.filter( d => d['source'] == msg );
2019-01-23 14:26:44 +00:00
}
2019-01-24 13:27:04 +00:00
getDirectionsTo( msg ) {
return this.directions.filter( d => d['target'] == msg );
2019-01-23 14:26:44 +00:00
}
addMsg(skipRebuild) {
2019-01-23 14:26:44 +00:00
let msg = {
2019-01-24 13:27:04 +00:00
"@id": this.language_code.substring( 0, 2 ) + "-n" + Date.now().toString( 36 ),
2019-01-23 14:26:44 +00:00
"@type": "Msg",
"text": "New",
"start": false,
"afterrunTime": 0.5,
2019-01-23 14:26:44 +00:00
}
2019-01-24 13:27:04 +00:00
this.data.push( msg );
console.log("skip or not to skip?", skipRebuild);
if(typeof skipRebuild == 'undefined' || !skipRebuild) {
this.updateFromData();
this.build();
this.selectMsg(msg);
}
2019-01-23 14:26:44 +00:00
return msg;
}
2019-01-24 13:27:04 +00:00
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 );
2019-01-23 14:26:44 +00:00
}
2019-01-24 13:27:04 +00:00
this._rmNode( msg );
2019-01-23 14:26:44 +00:00
}
2019-01-24 13:27:04 +00:00
_rmNode( node ) {
2019-01-23 14:26:44 +00:00
// remove msg/direction/condition/etc
2019-01-24 13:27:04 +00:00
let i = this.data.indexOf( node );
this.data.splice( i, 1 );
2019-01-23 14:26:44 +00:00
this.updateFromData();
this.build();
return this.data;
}
2019-01-24 13:27:04 +00:00
addConditionForDirection( type, label, vars, direction ) {
let con = this.addCondition( type, label, vars, true );
direction['conditions'].push( con['@id'] );
2019-01-23 14:26:44 +00:00
this.updateFromData();
this.build();
this.updateMsg();
}
2019-01-24 13:27:04 +00:00
addCondition( type, label, vars, skip ) {
2019-01-23 14:26:44 +00:00
let con = {
2019-01-24 13:27:04 +00:00
"@id": this.language_code.substring( 0, 2 ) + "-c" + Date.now().toString( 36 ),
2019-01-23 14:26:44 +00:00
"@type": "Condition",
"type": type,
"label": label,
"vars": vars
}
2019-01-24 13:27:04 +00:00
this.data.push( con );
if ( skip !== true ) {
2019-01-23 14:26:44 +00:00
this.updateFromData();
this.build();
}
return con;
}
2019-01-24 13:27:04 +00:00
addDirection( source, target ) {
2019-01-23 14:26:44 +00:00
let dir = {
2019-01-24 13:27:04 +00:00
"@id": this.language_code.substring( 0, 2 ) + "-d" + Date.now().toString( 36 ),
2019-01-23 14:26:44 +00:00
"@type": "Direction",
"source": source,
"target": target,
"conditions": []
}
2019-01-24 13:27:04 +00:00
this.data.push( dir );
let skipDistances;
2019-04-06 10:09:29 +00:00
// 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]];
2019-04-06 10:09:29 +00:00
// 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);
2019-01-23 14:26:44 +00:00
this.build();
return dir;
}
2019-01-24 13:27:04 +00:00
rmDirection( dir ) {
this._rmNode( dir );
// todo, remove orphaned conditions
2019-01-23 14:26:44 +00:00
}
createMsg() {
this.addMsg();
this.build();
}
2019-01-30 17:00:40 +00:00
createConnectedMsg(sourceMsg) {
console.time('createConnected');
console.time("Add");
let newMsg = this.addMsg(true); // skipRebuild = true, as addDirection() already rebuilds the graph
2019-02-28 17:58:03 +00:00
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");
2019-02-28 17:58:03 +00:00
console.time("direction");
2019-01-30 17:00:40 +00:00
this.addDirection(sourceMsg, newMsg);
console.timeEnd("direction");
console.time("build");
// this.build(); // build is already done in addDirection()
console.timeEnd("build");
2019-01-30 17:00:40 +00:00
// reselect so that overview is updated
console.time("Select");
2019-01-30 17:00:40 +00:00
this.selectMsg(newMsg);
console.timeEnd("Select");
console.timeEnd('createConnected');
2019-01-30 17:00:40 +00:00
}
2019-01-23 14:26:44 +00:00
2019-01-24 13:27:04 +00:00
getNodeById( id ) {
return this.data.filter( node => node['@id'] == id )[0];
2019-01-23 14:26:44 +00:00
}
/**
* Use wrapper method, because for event handlers 'this' will refer to
* the input object
*/
2019-02-18 19:38:54 +00:00
getEditEventListener(callback) {
2019-01-23 14:26:44 +00:00
let graph = this;
2019-01-24 13:27:04 +00:00
let el = function( e ) {
console.info("Changed", e);
2019-01-24 13:27:04 +00:00
let parts = e.srcElement.name.split( '-' );
let field = parts.pop();
let id = parts.join('-');
2019-01-24 13:27:04 +00:00
let node = graph.getNodeById( id );
let path = field.split( '.' ); // use vars.test to set ['vars']['test'] = value
var res = node;
let value = e.srcElement.value
if(e.srcElement.type == 'checkbox') {
value = e.srcElement.checked;
}
2019-01-24 13:27:04 +00:00
for ( var i = 0; i < path.length; i++ ) {
if ( i == ( path.length - 1 ) ) {
res[path[i]] = value;
2019-01-23 14:26:44 +00:00
} else {
2019-01-24 13:27:04 +00:00
res = res[path[i]];
2019-01-23 14:26:44 +00:00
}
}
// node[field] = e.srcElement.value;
graph.build();
2019-02-18 19:38:54 +00:00
if(typeof callback !== 'undefined'){
callback();
}
2019-01-23 14:26:44 +00:00
}
return el;
}
getJsonString() {
// recreate array to have the right order of items.
this.data = [...this.messages, ...this.conditions,
...this.directions, ...this.diversions]
2019-01-23 14:26:44 +00:00
let d = [];
2019-02-25 11:18:45 +00:00
// let toRemove = ['sourceX', 'sourceY', 'targetX', 'targetY', 'x', 'y', 'vx', 'vy']
let toRemove = ['sourceX', 'sourceY', 'targetX', 'targetY', 'vx', 'vy']
2019-01-24 13:27:04 +00:00
for ( let node of this.data ) {
2019-01-23 14:26:44 +00:00
let n = {};
2019-02-25 14:56:59 +00:00
// console.log( node['source'] );
2019-01-24 13:27:04 +00:00
for ( let e in node ) {
if ( node.hasOwnProperty( e ) && toRemove.indexOf( e ) == -1 ) {
if ( this.data.indexOf( node[e] ) != -1 ) {
2019-01-23 14:26:44 +00:00
n[e] = node[e]['@id'];
} else {
n[e] = node[e];
}
}
}
2019-01-24 13:27:04 +00:00
d.push( n );
2019-01-23 14:26:44 +00:00
}
console.info("Jsonified graph:",d);
2019-01-24 13:27:04 +00:00
return JSON.stringify( d );
2019-01-23 14:26:44 +00:00
}
2019-01-23 21:38:27 +00:00
downloadJson() {
2019-01-24 13:27:04 +00:00
if ( !this.language_code ) {
alert( "Make sure to load a language first" )
2019-01-23 21:38:27 +00:00
}
2019-01-24 13:27:04 +00:00
var blob = new Blob( [this.getJsonString()], { type: 'application/json' } );
if ( window.navigator.msSaveOrOpenBlob ) {
window.navigator.msSaveBlob( blob, "pillow_talk.json" );
2019-01-23 14:26:44 +00:00
}
2019-01-24 13:27:04 +00:00
else {
var elem = window.document.createElement( 'a' );
elem.href = window.URL.createObjectURL( blob );
2019-01-23 14:26:44 +00:00
elem.download = "pillow_talk.json";
2019-01-24 13:27:04 +00:00
document.body.appendChild( elem );
2019-01-23 14:26:44 +00:00
elem.click();
2019-01-24 13:27:04 +00:00
document.body.removeChild( elem );
2019-01-23 14:26:44 +00:00
}
}
2019-01-24 13:27:04 +00:00
2019-01-24 14:01:01 +00:00
saveJson( msg_id, fileInputElement, callback ) {
2019-01-24 13:27:04 +00:00
if ( !this.language_code ) {
alert( "Make sure to load a language first" )
2019-01-23 21:38:27 +00:00
}
2019-01-24 13:27:04 +00:00
2019-01-23 21:38:27 +00:00
let formData = new FormData();
2019-01-24 13:27:04 +00:00
formData.append( "language", this.language_code );
if ( msg_id ) {
formData.append( "message_id", msg_id );
formData.append( "audio", fileInputElement.files[0] );
2019-01-23 21:38:27 +00:00
}
2019-01-24 13:27:04 +00:00
let blob = new Blob( [this.getJsonString()], { type: "application/json" } );
formData.append( "json", blob );
console.info("Save json", formData );
2019-01-23 21:38:27 +00:00
var request = new XMLHttpRequest();
2019-01-24 13:27:04 +00:00
request.open( "POST", "http://localhost:8888/upload" );
2019-01-24 14:01:01 +00:00
if(callback) {
request.addEventListener( "load", callback);
}
2019-01-24 13:27:04 +00:00
request.send( formData );
2019-01-23 21:38:27 +00:00
}
2019-01-23 14:26:44 +00:00
2019-01-24 13:27:04 +00:00
loadData( data, language_code ) {
2019-01-23 21:38:27 +00:00
this.language_code = language_code;
2019-01-23 14:26:44 +00:00
this.data = data;
this.updateFromData();
2019-01-24 13:27:04 +00:00
this.build( true );
2019-01-23 14:26:44 +00:00
}
updateFromData(skipDistances) {
2019-01-24 13:27:04 +00:00
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' );
2019-01-24 13:27:04 +00:00
document.getElementById('current_lang').innerHTML = "";
document.getElementById('current_lang').appendChild(crel('span', {
'class': 'flag-icon ' + this.language_code
}));
2019-02-18 21:33:31 +00:00
if(typeof skipDistances == 'undefined' || !skipDistances) {
this.distances = this.calculateDistancesFromStart();
}
2019-01-23 14:26:44 +00:00
// save state;
this.saveState();
}
2019-01-25 13:10:19 +00:00
updateHugveyStatus(hv) {
let els = document.getElementsByClassName('beenHit');
while(els.length > 0) {
els[0].classList.remove('beenHit');
}
2019-01-25 14:45:46 +00:00
if(!hv || typeof hv['history'] == 'undefined') {
return;
}
2019-01-25 13:10:19 +00:00
for(let msg of hv['history']['messages']) {
document.getElementById(msg[0]['id']).classList.add('beenHit');
}
for(let msg of hv['history']['directions']) {
document.getElementById(msg[0]['id']).classList.add('beenHit');
}
}
2019-01-23 14:26:44 +00:00
saveState() {
2019-01-24 13:27:04 +00:00
window.localStorage.setItem( "lastState", this.getJsonString() );
2019-01-23 14:26:44 +00:00
}
hasSavedState() {
2019-01-24 13:27:04 +00:00
return window.localStorage.getItem( "lastState" ) !== null;
2019-01-23 14:26:44 +00:00
}
loadFromState() {
2019-01-24 13:27:04 +00:00
this.loadData( JSON.parse( window.localStorage.getItem( "lastState" ) ) );
2019-01-23 14:26:44 +00:00
}
2019-01-24 13:27:04 +00:00
build( isInit ) {
this.simulation = d3.forceSimulation( this.messages )
2019-02-18 21:33:31 +00:00
.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 ) )
2019-02-25 17:07:25 +00:00
.force( "collide", d3.forceCollide( this.nodeSize * 1.5 ).strength(3) )
2019-02-18 21:33:31 +00:00
.force( "forceX", d3.forceX(function(m){
2019-02-25 16:05:14 +00:00
let fx = panopticon.graph.distances[m['@id']] !== null ? panopticon.graph.distances[m['@id']][0] * panopticon.graph.nodeSize * 4 : 0
2019-02-25 14:56:59 +00:00
// console.log('fx', m['@id'], panopticon.graph.distances[m['@id']], fx);
2019-02-18 21:33:31 +00:00
return fx;
}).strength(50))
2019-02-25 16:05:14 +00:00
.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))
2019-02-25 11:18:45 +00:00
// .force( "forceY", d3.forceY(m => panopticon.graph.distances[m['@id']] !== null ? 0 : panopticon.graph.nodeSize * 3 ).strength(30))
2019-01-23 14:26:44 +00:00
;
2019-02-18 22:13:42 +00:00
this.simulation.velocityDecay(.99);
2019-01-23 14:26:44 +00:00
// Update existing nodes
let node = this.nodesG
2019-01-24 13:27:04 +00:00
.selectAll( "g" )
.data( this.messages, n => n['@id'] )
;
2019-02-25 11:18:45 +00:00
2019-01-23 14:26:44 +00:00
// Update existing nodes
let newNode = node.enter();
2019-01-24 13:27:04 +00:00
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)
;
2019-01-30 17:00:40 +00:00
2019-01-24 13:27:04 +00:00
let textId = newNodeG.append( "text" ).attr( 'class', 'msg_id' );
let textContent = newNodeG.append( "text" ).attr( 'class', 'msg_txt' );
2019-01-24 14:27:22 +00:00
let statusIcon = newNodeG.append( "image" )
.attr( 'class', 'status_icon' )
.attr( 'x', '-10' )
.attr( 'y', '10' )
.attr( 'width', '20' )
.attr( 'height', '20' )
;
2019-01-24 13:27:04 +00:00
// remove
node.exit().remove();
node = node.merge( newNodeG );
2019-02-25 11:18:45 +00:00
2019-01-24 13:27:04 +00:00
// 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' );
2019-01-24 13:27:04 +00:00
if ( this.getDirectionsFrom( msg ).length < 1 ) {
classes.push( 'endMsg' );
if ( this.getDirectionsTo( msg ).length < 1 ) classes.push( 'orphanedMsg' );
}
2019-01-23 14:26:44 +00:00
2019-01-24 13:27:04 +00:00
return classes.join( ' ' );
} )
2019-02-25 11:18:45 +00:00
.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'))
);
2019-02-28 17:58:03 +00:00
node.select('circle').attr('style', (d) => 'fill: ' + (d.hasOwnProperty('color') ? d['color'] : '#77618e'));
2019-01-24 13:27:04 +00:00
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']}` ) );
2019-01-24 14:27:22 +00:00
node.selectAll( "image.status_icon" ).attr('xlink:href', d => d['audio'] ? '' : '/images/music-broken.svg');
2019-01-24 13:27:04 +00:00
// 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", () => {
2019-01-23 14:26:44 +00:00
link
2019-01-24 13:27:04 +00:00
.each( function( d ) {
2019-01-23 14:26:44 +00:00
let sourceX, targetX, midX, dx, dy, angle;
// This mess makes the arrows exactly perfect.
// thanks to http://bl.ocks.org/curran/9b73eb564c1c8a3d8f3ab207de364bf4
2019-01-24 13:27:04 +00:00
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;
2019-01-23 14:26:44 +00:00
} else {
2019-01-24 13:27:04 +00:00
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;
2019-01-23 14:26:44 +00:00
}
dx = targetX - sourceX;
dy = d.target.y - d.source.y;
2019-01-24 13:27:04 +00:00
angle = Math.atan2( dx, dy );
2019-01-23 14:26:44 +00:00
// Compute the line endpoint such that the arrow
// is touching the edge of the node rectangle perfectly.
2019-01-24 13:27:04 +00:00
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();
2019-02-25 11:18:45 +00:00
for ( let i = 0, n = Math.ceil( Math.log( this.simulation.alphaMin() ) / Math.log( 1 - this.simulation.alphaDecay() ) ); i < n; ++i ) {
this.simulation.tick();
2019-01-23 14:26:44 +00:00
}
2019-01-24 13:27:04 +00:00
return this.svg.node();
}
2019-02-18 21:33:31 +00:00
calculateDistancesFromStart() {
2019-03-25 16:45:07 +00:00
console.time('calculateDistancesFromStart');
2019-02-18 21:33:31 +00:00
let starts = this.messages.filter( m => m.hasOwnProperty('start') && m['start'] == true);
if (starts.length < 1) {
console.error("No start set");
return;
}
2019-02-25 11:18:45 +00:00
2019-02-18 21:33:31 +00:00
//initiate distances
let distances = {};
for(let msg of this.messages) {
2019-02-25 11:18:45 +00:00
// distances[msg['@id']] = msg === startMsg ? 0 : null;
distances[msg['@id']] = null;
2019-02-18 21:33:31 +00:00
}
2019-02-18 22:13:42 +00:00
let targetsPerMsg = {};
let sourcesPerMsg = {};
2019-02-25 14:56:59 +00:00
// console.log("dir", this.directions);
2019-02-18 21:33:31 +00:00
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'];
2019-02-18 22:13:42 +00:00
if(!targetsPerMsg.hasOwnProperty(from)) {
targetsPerMsg[from] = [];
2019-02-18 21:33:31 +00:00
}
2019-02-18 22:13:42 +00:00
targetsPerMsg[from].push(to);
if(!sourcesPerMsg.hasOwnProperty(to)) {
sourcesPerMsg[to] = [];
}
sourcesPerMsg[to].push(from);
2019-02-18 21:33:31 +00:00
}
2019-02-25 16:05:14 +00:00
let traverseMsg = function(msgId, depth, goingDown, yPos) {
2019-02-18 22:13:42 +00:00
let msgsPerMsg = goingDown ? targetsPerMsg : sourcesPerMsg;
2019-02-25 14:56:59 +00:00
// console.log(goingDown, msgId, depth);
2019-02-18 22:13:42 +00:00
if(!msgsPerMsg.hasOwnProperty(msgId)) {
2019-02-18 21:33:31 +00:00
// end of trail
2019-02-25 16:05:14 +00:00
return yPos;
2019-02-18 21:33:31 +00:00
}
2019-02-25 16:05:14 +00:00
2019-02-25 17:07:25 +00:00
let i = 0, y =0;
2019-02-18 22:13:42 +00:00
for(let childMsgId of msgsPerMsg[msgId]) {
if(distances[childMsgId] !== null){
continue;
}
2019-02-25 16:05:14 +00:00
if(distances[childMsgId] === null || (goingDown && distances[childMsgId][0] > depth)) {
2019-02-25 17:07:25 +00:00
if(distances[childMsgId] === null) {
if(i > 0){
yPos++;
}
i++;
console.log('set for id', childMsgId, goingDown, depth, yPos);
distances[childMsgId] = [depth, yPos];
}
else{
y++;
}
2019-02-18 22:13:42 +00:00
// console.log(goingDown, childMsgId, depth);
2019-02-25 16:05:14 +00:00
yPos = traverseMsg(childMsgId, goingDown ? (depth+1) : (depth - 1), goingDown, yPos);
2019-02-18 22:13:42 +00:00
}
2019-02-25 16:05:14 +00:00
else if(!goingDown && distances[childMsgId][0] < depth) {
2019-02-25 17:07:25 +00:00
if(childMsgId == 'en-njsgkr4az') {
console.log('set for id', childMsgId, goingDown);
}
if(distances[childMsgId] === null) {
distances[childMsgId] = [depth, yPos];
}
2019-02-18 22:13:42 +00:00
// console.log('a', depth);
2019-02-25 16:05:14 +00:00
yPos = traverseMsg(childMsgId, depth - 1, goingDown, yPos);
2019-02-18 21:33:31 +00:00
} else {
// apparently, there is a loop. Don't traverse it.
}
2019-02-25 16:05:14 +00:00
2019-02-18 21:33:31 +00:00
}
2019-02-25 17:07:25 +00:00
// if( i == 0 && y == 1) {
// // we reached an item that branches back into the tree
// return yPos -1;
// }
// console.log('yPos',msgId,yPos);
2019-02-25 16:05:14 +00:00
return yPos;
2019-02-18 21:33:31 +00:00
}
2019-02-25 16:05:14 +00:00
let yPos = 0;
2019-03-25 16:45:07 +00:00
console.time('step1');
2019-02-25 11:18:45 +00:00
for(let startMsg of starts) {
2019-03-25 16:45:07 +00:00
console.time('start: '+startMsg['@id']);
2019-02-25 11:18:45 +00:00
if(distances[startMsg['@id']] === null) {
2019-02-25 16:05:14 +00:00
distances[startMsg['@id']] = [0, yPos];
2019-02-25 11:18:45 +00:00
}
2019-02-25 16:05:14 +00:00
yPos = traverseMsg(startMsg['@id'], 1 , true, yPos);
yPos += 1;
2019-03-25 16:45:07 +00:00
console.timeEnd('start: '+startMsg['@id']);
2019-02-25 11:18:45 +00:00
}
2019-03-25 16:45:07 +00:00
console.timeEnd('step1');
console.time('step2');
2019-02-18 22:13:42 +00:00
// now we have the formal tree, lets try to polish the rest:
for(let msgId in distances) {
2019-03-25 16:45:07 +00:00
console.time('polish: '+ msgId);
2019-02-18 22:13:42 +00:00
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
2019-02-25 16:05:14 +00:00
traverseMsg(msgId, distances[msgId][0] -1, false, distances[msgId][1])
2019-03-25 16:45:07 +00:00
console.timeEnd('polish: '+ msgId);
2019-02-18 22:13:42 +00:00
}
2019-03-25 16:45:07 +00:00
console.timeEnd('step2');
2019-02-18 22:13:42 +00:00
// 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);
//
// }
2019-03-25 16:45:07 +00:00
console.timeEnd("calculateDistancesFromStart");
2019-02-18 21:33:31 +00:00
return distances;
}
2019-01-23 14:26:44 +00:00
}
//
2019-04-25 17:08:27 +00:00
//