hugvey/www/js/hugvey_console.js

1028 lines
35 KiB
JavaScript

var panopticon;
class Panopticon {
constructor() {
console.log( "Init panopticon" );
this.languages = []
// this.selectedHugvey = null;
this.hugveys = new Vue( {
el: "#interface",
data: {
uptime: 0,
languages: [],
hugveys: [],
selectedId: null,
},
methods: {
time_passed: function( hugvey, property ) {
console.log( "property!", Date( hugvey[property] * 1000 ) );
return moment( Date( hugvey[property] * 1000 ) ).fromNow();
},
timer: function(hugvey, property) {
return panopticon.stringToHHMMSS( hugvey[property] );
},
loadNarrative: function( code, file ) {
panopticon.hugveys.selectedId = null;
return panopticon.loadNarrative( code, file );
},
pause: function(hv) {
hv.status = "loading";
return panopticon.pause(hv.id);
},
resume: function(hv) {
hv.status = "loading";
return panopticon.resume(hv.id);
},
restart: function(hv) {
hv.status = "loading";
return panopticon.restart(hv.id);
},
change_lang: function(hv, lang_code) {
hv.status = "loading";
return panopticon.change_language(hv.id, lang_code);
},
showHugvey: function(hv) {
panopticon.hugveys.selectedId = hv.language ? hv.id : null;
panopticon.updateSelectedHugvey();
}
}
} );
this.socket = new ReconnectingWebSocket( "ws://localhost:8888/ws", null, { debug: false, reconnectInterval: 3000 } );
this.graph = new Graph();
this.socket.addEventListener( 'open', ( e ) => {
this.send( { action: 'init' } );
} );
this.socket.addEventListener( 'close', function( e ) {
console.log( 'Closed connection' );
} );
this.socket.addEventListener( 'message', ( e ) => {
let msg = JSON.parse( e.data );
if ( typeof msg['alert'] !== 'undefined' ) {
alert( msg['alert'] );
}
if ( typeof msg['action'] === 'undefined' ) {
console.error( "not a valid message: " + e.data );
return;
}
switch ( msg['action'] ) {
case 'status':
console.debug(msg);
this.hugveys.uptime = this.stringToHHMMSS( msg['uptime'] );
this.hugveys.languages = msg['languages'];
this.languages = msg['languages'];
this.hugveys.hugveys = msg['hugveys'];
if(this.hugveys.selectedId) {
this.updateSelectedHugvey();
}
break;
}
} );
}
updateSelectedHugvey() {
let hv = null;
if(this.hugveys.selectedId) {
hv = this.getHugvey(this.hugveys.selectedId);
if(hv.language && this.graph.language_code != hv.language) {
this.loadNarrative(hv.language);
}
}
this.graph.updateHugveyStatus(hv);
}
getHugvey(id) {
for(let hv of this.hugveys.hugveys) {
if(hv.id == id) {
return hv;
}
}
return null;
}
send( msg ) {
if ( this.socket.readyState == WebSocket.OPEN ) {
this.socket.send( JSON.stringify( msg ) );
} else {
console.error( "Socket not open: ", this.socket.readyState );
}
}
getStatus() {
// console.log('get status', this, panopticon);
panopticon.send( { action: 'get_status' } );
}
init() {
setInterval( this.getStatus, 3000 );
}
stringToHHMMSS( string ) {
var sec_num = parseInt( string, 10 ); // don't forget the second param
var hours = Math.floor( sec_num / 3600 );
var minutes = Math.floor(( sec_num - ( hours * 3600 ) ) / 60 );
var seconds = sec_num - ( hours * 3600 ) - ( minutes * 60 );
if ( hours < 10 ) { hours = "0" + hours; }
if ( minutes < 10 ) { minutes = "0" + minutes; }
if ( seconds < 10 ) { seconds = "0" + seconds; }
return hours + ':' + minutes + ':' + seconds;
}
loadNarrative( code, file ) {
if(typeof file == 'undefined') {
for (let lang of this.languages) {
if (lang['code'] == code) {
file = lang['file'];
}
}
}
let req = new XMLHttpRequest();
let graph = this.graph;
req.addEventListener( "load", function( e ) {
graph.loadData( JSON.parse( this.response ), code );
// console.log(, e);
} );
req.open( "GET", "/local/" + file );
req.send();
}
resume( hv_id ) {
this.send( { action: 'resume', hugvey: hv_id } )
}
pause( hv_id ) {
this.send( { action: 'pause', hugvey: hv_id } )
}
restart( hv_id ) {
this.send( { action: 'restart', hugvey: hv_id } );
}
change_language( hv_id, lang_code ) {
this.send( { action: 'change_language', hugvey: hv_id, lang_code: lang_code } );
}
playFromSelected(msg_id) {
if(!this.hugveys.selectedId) {
alert('No hugvey selected');
} else {
this.send({ action: 'play_msg', hugvey: this.hugveys.selectedId, msg_id: msg_id })
}
}
}
window.addEventListener( 'load', function() {
panopticon = new Panopticon();
panopticon.init();
} );
class Graph {
constructor() {
this.width = 1280;
this.height = 1024;
this.nodeSize = 80;
this.maxChars = 16;
this.svg = d3.select( '#graph' );
this.container = d3.select( '#container' );
this.selectedMsg = null;
this.language_code = null;
this.messages = []; // initialise empty array. For the simulation, make sure we keep the same array object
this.directions = []; // initialise empty array. For the simulation, make sure we keep the same array object
this.conditions = []; // initialise empty array. For the simulation, make sure we keep the same array object
this.diversions = []; // initialise empty array. For the simulation, make sure we keep the same array object
let graph = this;
this.controlDown = false;
document.addEventListener( 'keydown', function( e ) {
console.log( e );
if ( e.which == "17" ) {
graph.controlDown = true;
document.body.classList.add( 'controlDown' );
}
} );
document.addEventListener( 'keyup', function( e ) {
console.log( e );
if ( e.which == "17" ) {
graph.controlDown = false;
document.body.classList.remove( 'controlDown' );
}
} );
let c = this.container;
let zoomed = function() {
c.attr( "transform", d3.event.transform );
}
this.svg.call( d3.zoom()
.scaleExtent( [1 / 2, 8] )
.on( "zoom", zoomed ) );
this.nodesG = this.container.append( "g" )
.attr( "id", "nodes" )
this.linkG = this.container.append( "g" )
.attr( "id", "links" );
document.getElementById( 'btn-save' ).addEventListener( 'click', function( e ) { graph.saveJson(); } );
document.getElementById( 'btn-addMsg' ).addEventListener( 'click', function( e ) { graph.createMsg(); } );
}
clickMsg( msg ) {
// event when a message is clicked.
console.log( msg );
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 );
}
showMsg( msg ) {
let msgEl = document.getElementById( 'msg' );
msgEl.innerHTML = "";
if(msg == null){
return;
}
let startAttributes = {
'name': msg['@id'] + '-start',
'disabled': true,
'type': 'checkbox',
'on': {
'change': this.getEditEventListener()
}
}
if ( msg['start'] == true ) {
startAttributes['checked'] = 'checked';
}
let audioSpan = crel(
'span',
{
'title': msg['audio'] ? msg['audio']['file'] : "",
'class': "label-value",
},
msg['audio'] ? msg['audio']['original_name'] : ""
);
if(msg['audio']) {
audioSpan.appendChild(crel(
'audio', {'controls': 'controls'},
crel('source', {'src':msg['audio']['file']})
));
}
let msgInfoEl = crel( 'div', { 'class': 'msg__info' },
crel('div', {
'class':'btn btn--delete',
'on': {
'click': function(e) {
if(confirm(`Are you sure you want to remove message ${msg['@id']}`)) {
panopticon.graph.rmMsg(msg);
panopticon.graph.showMsg(null);
}
}
}
}, 'delete'),
crel( 'h1', { 'class': 'msg__id' }, msg['@id'] ),
crel( 'label',
crel( 'span', 'Text' ),
crel( 'input', {
'name': msg['@id'] + '-text',
'value': msg['text'],
'on': {
'change': this.getEditEventListener()
}
} )
),
crel( 'label',
crel( 'span', 'Start' ),
crel( 'input', startAttributes )
),
crel( 'label',
crel( 'span', 'Audio' ),
audioSpan,
crel( 'input', {
'type': 'file',
'name': 'audio',
'accept': '.wav,.ogg,.mp3',
'on': {
'change': function(e) {
audioSpan.innerHTML = "...";
panopticon.graph.saveJson(msg['@id'], e.target, function(e2){
console.log(e);
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);
}
}
} )
)
);
msgEl.appendChild( msgInfoEl );
if(panopticon.hugveys.selectedId) {
let playEl = crel(
'div',
{'class': 'play'},
crel(
'div', {
'class': 'btn btn--play',
'on': {
'click': function (e) {
console.log('go save');
panopticon.graph.saveJson(null, null, function(){
console.log('saved, now play');
panopticon.playFromSelected(msg['@id']);
});
}
}
},
"Save & play on #" + panopticon.hugveys.selectedId
)
);
msgEl.appendChild(playEl);
}
// let directionHEl = document.createElement('h2');
// directionHEl.innerHTML = "Directions";
let fromDirections = [], toDirections = [];
for ( let direction of this.getDirectionsTo( msg ) ) {
toDirections.push( this.getDirectionEl( direction, msg ) );
}
for ( let direction of this.getDirectionsFrom( msg ) ) {
fromDirections.push( this.getDirectionEl( direction, msg ) );
}
let directionsEl = crel( 'div', { 'class': 'directions' },
crel( 'h2', 'Directions' ),
...toDirections, ...fromDirections
);
msgEl.appendChild( directionsEl );
}
getDirectionEl( direction, msg ) {
let g = this;
let directionEl = crel('div',
{
'class': 'direction ' + (direction['source'] == msg ? 'dir-to' : 'dir-from'),
'on': {
'mouseover': function(e) {
directionEl.classList.add('dir-highlight');
document.getElementById(direction['@id']).classList.add('dir-highlight');
},
'mouseout': function(e) {
directionEl.classList.remove('dir-highlight');
document.getElementById(direction['@id']).classList.remove('dir-highlight');
}
}
},
crel(
'h3',
{'title': direction['@id']},
direction['source'] == msg ? `To ${direction['target']['@id']}`: `From ${direction['source']['@id']}`
),
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?")) {
panopticon.graph.rmCondition( condition, direction );
}
}
}
}, 'delete')
)
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() {
if ( typeof this.conditionTypes === 'undefined' ) {
// type: vars: attribtes for crel()
this.conditionTypes = {
'timeout': {
'seconds': { 'type': 'number', 'value': 10, 'min': 0, 'step': 0.1 }
},
'replyContains': {
'regex': { 'value': '.+' }
}
}
}
return this.conditionTypes;
}
fillConditionFormForType( conditionForm, type ) {
conditionForm.innerHTML = "";
let vars = this.getConditionTypes()[type];
for ( let v in vars ) {
let attr = vars[v];
attr['name'] = v;
conditionForm.appendChild(
crel( 'label',
crel( 'span', v ),
crel( 'input', attr )
)
);
}
}
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' );
let vars = {};
for ( var pair of form.entries() ) {
vars[pair[0]] = pair[1];
}
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);
if(pos > -1) {
direction['conditions'].splice(pos, 1);
}
for(let dir of this.directions) {
if(dir['conditions'].indexOf(id) > 0) {
console.log("Condition still in use");
this.updateFromData();
this.build();
this.updateMsg();
return;
}
}
this._rmNode( id );
} else {
for(let dir of this.directions) {
let pos = dir['conditions'].indexOf(id);
if(pos > -1) {
dir['conditions'].splice(pos, 1);
}
}
this._rmNode( id );
}
this.updateMsg();
}
getConditionEl( condition ) {
let conditionEl = document.createElement( 'div' );
return conditionEl;
}
getDirectionsFrom( msg ) {
return this.directions.filter( d => d['source'] == msg );
}
getDirectionsTo( msg ) {
return this.directions.filter( d => d['target'] == msg );
}
addMsg() {
let msg = {
"@id": this.language_code.substring( 0, 2 ) + "-n" + Date.now().toString( 36 ),
"@type": "Msg",
"text": "New",
"start": false
}
this.data.push( msg );
this.updateFromData();
this.build();
this.selectMsg(msg);
return msg;
}
rmMsg( msg ) {
let invalidatedDirections = this.directions.filter( d => d['source'] == msg || d['target'] == msg );
console.log( 'invalidated', invalidatedDirections );
for ( let dir of invalidatedDirections ) {
let i = this.data.indexOf( dir );
this.data.splice( i, 1 );
}
this._rmNode( msg );
}
_rmNode( node ) {
// remove msg/direction/condition/etc
let i = this.data.indexOf( node );
this.data.splice( i, 1 );
this.updateFromData();
this.build();
return this.data;
}
addConditionForDirection( type, label, vars, direction ) {
let con = this.addCondition( type, label, vars, true );
direction['conditions'].push( con['@id'] );
this.updateFromData();
this.build();
this.updateMsg();
}
addCondition( type, label, vars, skip ) {
let con = {
"@id": this.language_code.substring( 0, 2 ) + "-c" + Date.now().toString( 36 ),
"@type": "Condition",
"type": type,
"label": label,
"vars": vars
}
this.data.push( con );
if ( skip !== true ) {
this.updateFromData();
this.build();
}
return con;
}
addDirection( source, target ) {
let dir = {
"@id": this.language_code.substring( 0, 2 ) + "-d" + Date.now().toString( 36 ),
"@type": "Direction",
"source": source,
"target": target,
"conditions": []
}
this.data.push( dir );
this.updateFromData();
this.build();
return dir;
}
rmDirection( dir ) {
this._rmNode( dir );
// todo, remove orphaned conditions
}
createMsg() {
this.addMsg();
this.build();
}
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() {
let graph = this;
let el = function( e ) {
let parts = e.srcElement.name.split( '-' );
let field = parts.pop();
let id = parts.join('-');
console.log( this, graph );
let node = graph.getNodeById( id );
let path = field.split( '.' ); // use vars.test to set ['vars']['test'] = value
console.log(id, node);
var res = node;
for ( var i = 0; i < path.length; i++ ) {
if ( i == ( path.length - 1 ) ) {
console.log( 'last', path[i] );
res[path[i]] = e.srcElement.value;
} else {
res = res[path[i]];
}
}
// node[field] = e.srcElement.value;
graph.build();
}
return el;
}
getJsonString() {
// recreate array to have the right order of items.
this.data = [...this.messages, ...this.conditions,
...this.directions, ...this.diversions]
let d = [];
let toRemove = ['sourceX', 'sourceY', 'targetX', 'targetY', 'x', 'y', 'vx', 'vy']
for ( let node of this.data ) {
let n = {};
console.log( node['source'] );
for ( let e in node ) {
if ( node.hasOwnProperty( e ) && toRemove.indexOf( e ) == -1 ) {
if ( this.data.indexOf( node[e] ) != -1 ) {
n[e] = node[e]['@id'];
} else {
n[e] = node[e];
}
}
}
d.push( n );
}
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.log( formData );
var request = new XMLHttpRequest();
request.open( "POST", "http://localhost:8888/upload" );
if(callback) {
request.addEventListener( "load", callback);
}
request.send( formData );
}
loadData( data, language_code ) {
this.language_code = language_code;
this.data = data;
this.updateFromData();
this.build( true );
}
updateFromData() {
this.messages = this.data.filter(( node ) => node['@type'] == 'Msg' );
this.directions = this.data.filter(( node ) => node['@type'] == 'Direction' );
this.conditions = this.data.filter(( node ) => node['@type'] == 'Condition' );
this.diversions = this.data.filter(( node ) => node['@type'] == 'Diversion' );
document.getElementById('current_lang').innerHTML = "";
document.getElementById('current_lang').appendChild(crel('span', {
'class': 'flag-icon ' + this.language_code
}));
// save state;
this.saveState();
}
updateHugveyStatus(hv) {
let els = document.getElementsByClassName('beenHit');
while(els.length > 0) {
els[0].classList.remove('beenHit');
}
if(!hv || typeof hv['history'] == 'undefined') {
return;
}
for(let msg of hv['history']['messages']) {
document.getElementById(msg[0]['id']).classList.add('beenHit');
}
for(let msg of hv['history']['directions']) {
document.getElementById(msg[0]['id']).classList.add('beenHit');
}
}
saveState() {
window.localStorage.setItem( "lastState", this.getJsonString() );
}
hasSavedState() {
return window.localStorage.getItem( "lastState" ) !== null;
}
loadFromState() {
this.loadData( JSON.parse( window.localStorage.getItem( "lastState" ) ) );
}
build( isInit ) {
this.simulation = d3.forceSimulation( this.messages )
.force( "link", d3.forceLink( this.directions ).id( d => d['@id'] ) )
.force( "charge", d3.forceManyBody().strength( -1000 ) )
.force( "center", d3.forceCenter( this.width / 2, this.height / 2 ) )
.force( "collide", d3.forceCollide( this.nodeSize * 2 ) )
;
// Update existing nodes
let node = this.nodesG
.selectAll( "g" )
.data( this.messages, n => n['@id'] )
;
// Update existing nodes
let newNode = node.enter();
let newNodeG = newNode.append( "g" )
.attr( 'id', d => d['@id'] )
.call( d3.drag( this.simulation ) )
.on( 'click', function( d ) {
this.clickMsg( d );
}.bind( this ) )
;
console.log( 'a' );
let circle = newNodeG.append( "circle" )
.attr( 'r', this.nodeSize )
// .text(d => d.id)
;
let textId = newNodeG.append( "text" ).attr( 'class', 'msg_id' );
let textContent = newNodeG.append( "text" ).attr( 'class', 'msg_txt' );
let statusIcon = newNodeG.append( "image" )
.attr( 'class', 'status_icon' )
.attr( 'x', '-10' )
.attr( 'y', '10' )
.attr( 'width', '20' )
.attr( 'height', '20' )
;
// remove
node.exit().remove();
node = node.merge( newNodeG );
// for all existing nodes:
node.attr( 'class', msg => {
let classes = [];
if ( this.selectedMsg == msg ) classes.push( 'selectedMsg' );
if ( msg['start'] == true ) classes.push( 'startMsg' );
if ( this.getDirectionsFrom( msg ).length < 1 ) {
classes.push( 'endMsg' );
if ( this.getDirectionsTo( msg ).length < 1 ) classes.push( 'orphanedMsg' );
}
return classes.join( ' ' );
} )
let link = this.linkG
.selectAll( "line" )
.data( this.directions )
;
let newLink = link.enter()
.append( "line" )
;
//remove
link.exit().remove();
link = link.merge( newLink );
link.attr( 'class', l => { return `link ` + ( l['conditions'].length == 0 ? "link--noconditions" : "link--withconditions" ); } );
link.attr('id', (l) => l['@id']);
// console.log('c');
let formatText = ( t ) => {
if ( t.length > this.maxChars ) {
return t.substr( 0, this.maxChars - 3 ) + '...';
} else {
return t;
}
};
node.selectAll( "text.msg_id" ).text( d => d['@id'] );
node.selectAll( "text.msg_txt" ).text( d => formatText( `${d['text']}` ) );
node.selectAll( "image.status_icon" ).attr('xlink:href', d => d['audio'] ? '' : '/images/music-broken.svg');
// console.log('q');
// // TODO: update text
// let text = newNodeG.append("text")
// // .attr('stroke', "black")
// .text(d => formatText(`(${d['@id']}) ${d['text']}`))
// // .attr('title', d => d.label)
// ;
let n = this.nodesG;
this.simulation.on( "tick", () => {
link
.each( function( d ) {
let sourceX, targetX, midX, dx, dy, angle;
// This mess makes the arrows exactly perfect.
// thanks to http://bl.ocks.org/curran/9b73eb564c1c8a3d8f3ab207de364bf4
if ( d.source.x < d.target.x ) {
sourceX = d.source.x;
targetX = d.target.x;
} else if ( d.target.x < d.source.x ) {
targetX = d.target.x;
sourceX = d.source.x;
} else if ( d.target.isCircle ) {
targetX = sourceX = d.target.x;
} else if ( d.source.isCircle ) {
targetX = sourceX = d.source.x;
} else {
midX = ( d.source.x + d.target.x ) / 2;
if ( midX > d.target.x ) {
midX = d.target.x;
} else if ( midX > d.source.x ) {
midX = d.source.x;
} else if ( midX < d.target.x ) {
midX = d.target.x;
} else if ( midX < d.source.x ) {
midX = d.source.x;
}
targetX = sourceX = midX;
}
dx = targetX - sourceX;
dy = d.target.y - d.source.y;
angle = Math.atan2( dx, dy );
// Compute the line endpoint such that the arrow
// is touching the edge of the node rectangle perfectly.
d.sourceX = sourceX + Math.sin( angle ) * this.nodeSize;
d.targetX = targetX - Math.sin( angle ) * this.nodeSize;
d.sourceY = d.source.y + Math.cos( angle ) * this.nodeSize;
d.targetY = d.target.y - Math.cos( angle ) * this.nodeSize;
}.bind( this ) )
.attr( "x1", function( d ) { return d.sourceX; } )
.attr( "y1", function( d ) { return d.sourceY; } )
.attr( "x2", function( d ) { return d.targetX; } )
.attr( "y2", function( d ) { return d.targetY; } );
node.attr( "transform", d => `translate(${d.x},${d.y})` );
// .attr("cy", d => d.y);
} );
// this.simulation.alpha(1);
// this.simulation.restart();
if ( typeof isInit != 'undefined' && isInit ) {
for ( let i = 0, n = Math.ceil( Math.log( this.simulation.alphaMin() ) / Math.log( 1 - this.simulation.alphaDecay() ) ); i < n; ++i ) {
this.simulation.tick();
}
}
return this.svg.node();
}
}