mirror of
https://github.com/gsi-upm/soil
synced 2025-09-14 12:12:21 +00:00
The web server is included as a submodule. The dependencies for the web (tornado) are not installed by default, but they can be installed as an extra: ``` pip install soil[web] ``` Once installed, the soil web can be used like this: ``` soil-web OR python -m soil.web ``` There are other minor changes: * History re-connects to the sqlite database if it is used from a different thread. * Environment accepts additional parameters (so it can run simulations with `visualization_params` or any other in the future). * The simulator class is no longer necessary * Logging is done in the same thread, and the simulation is run in a separate one. This had to be done because it was creating some problems with tornado not being able to find the current thread during logs, which caused hundreds of repeated lines in the web "console". * The player is slightly modified in this version. I noticed that when the visualization was playing, if you clicked somewhere it would change for a second, and then go back to the previous place. The code for the playback seemed too complex, especially speed control, so I rewrote some parts. I might've introduced new bugs.
462 lines
16 KiB
JavaScript
Executable File
462 lines
16 KiB
JavaScript
Executable File
|
|
// Open the websocket connection
|
|
var ws = new WebSocket((window.location.protocol === 'https:' ? 'wss://' : 'ws://') + window.location.host + '/ws');
|
|
|
|
// Open conection with Socket
|
|
ws.onopen = function() {
|
|
console.log('Connection opened!');
|
|
};
|
|
|
|
// Receive data from server
|
|
ws.onmessage = function(message) {
|
|
//console.log('Message received!');
|
|
|
|
var msg = JSON.parse(message.data);
|
|
|
|
switch(msg['type']) {
|
|
case 'trials':
|
|
reset_trials();
|
|
set_trials(msg['data']);
|
|
// $('#load').removeClass('loader');
|
|
break;
|
|
|
|
case 'get_trial':
|
|
console.log(msg['data']);
|
|
|
|
self.GraphVisualization.import(convertJSON(msg['data']), function() {
|
|
reset_configuration();
|
|
set_configuration();
|
|
// $('#home_menu').click(function() {
|
|
// setTimeout(function() {
|
|
// reset_timeline();
|
|
// set_timeline(msg['data']);
|
|
// }, 1000);
|
|
// });
|
|
reset_timeline();
|
|
set_timeline(msg['data']);
|
|
$('#load').hide();
|
|
});
|
|
$('#charts .chart').removeClass('no-data');
|
|
set_chart_nodes(msg['data'], chart_nodes)
|
|
set_chart_attrs(msg['data'], chart_attrs, $('.config-item #properties').val())
|
|
$('.config-item #properties').change(function() {
|
|
chart_attrs.destroy();
|
|
chart_attrs = create_chart(width_chart, height_chart, 'Time', 'Attributes', '#chart_attrs');
|
|
set_chart_attrs(msg['data'], chart_attrs, $('.config-item #properties').val())
|
|
});
|
|
break;
|
|
|
|
case 'settings':
|
|
$('#wrapper-settings').empty().removeClass('none');
|
|
initGUI(msg['data']);
|
|
break;
|
|
|
|
case 'error':
|
|
console.error(msg['error']);
|
|
_socket.error(msg['error']);
|
|
$('#load').removeClass('loader');
|
|
break;
|
|
|
|
case 'log':
|
|
$('.console').append('$ ' + msg['logger'] + ': ' + msg['logging'] + '<br/>');
|
|
$('.console').animate({ scrollTop: $('.console')[0].scrollHeight }, 'fast');
|
|
break;
|
|
|
|
case 'visualization_params':
|
|
console.log(msg['data']);
|
|
self.GraphVisualization.set_params(msg['data']['shape_property'], msg['data']['shapes'], msg['data']['colors']);
|
|
|
|
if ( msg['data']['background_image'] ) {
|
|
// $('svg#graph').css('background-image', 'linear-gradient(to bottom, rgba(0,0,0,0.4) 0%,rgba(0,0,0,0.4) 100%), url(img/background/' + msg['data']['background_image'])
|
|
// .css('background-size', '130%').css('background-position', '5% 30%').css('background-repeat', 'no-repeat');
|
|
$('<style>').text('svg line.link { stroke: white !important; stroke-width: 1.5px !important; }').appendTo($('html > head'));
|
|
$('<style>').text('svg circle.node { stroke-width: 2.5px !important; }').appendTo($('html > head'));
|
|
self.GraphVisualization.set_background('img/background/' + msg['data']['background_image'], msg['data']['background_opacity'], msg['data']['background_filter_color']);
|
|
}
|
|
break;
|
|
|
|
case 'download_gexf':
|
|
var xml_declaration = '<?xml version="1.0" encoding="utf-8"?>';
|
|
download(msg['filename'] + '.gexf', 'xml', xml_declaration + msg['data']);
|
|
break;
|
|
|
|
case 'download_json':
|
|
download(msg['filename'] + '.json', 'json', JSON.stringify(msg['data'], null, 4));
|
|
break;
|
|
|
|
default:
|
|
console.warn('Unexpected message!')
|
|
}
|
|
|
|
}
|
|
|
|
var _socket = {
|
|
send: function(message, type) {
|
|
var json = {}
|
|
json['type'] = type
|
|
json['data'] = message
|
|
ws.send(JSON.stringify(json))
|
|
},
|
|
error: function(message) {
|
|
$('#error-message').text(message);
|
|
$('.alert.alert-danger').show();
|
|
},
|
|
current_trial: undefined
|
|
};
|
|
|
|
var set_trials = function(trials) {
|
|
for ( i in trials ) {
|
|
var list_item = $('<li>').appendTo('.dropdown#trials .dropdown-menu');
|
|
$('<a>').val(i).text(trials[i]).appendTo(list_item);
|
|
}
|
|
// Select 'trials'
|
|
$('.dropdown#trials li a').click(function() {
|
|
var a = $('.dropdown-toggle .caret');
|
|
$('.dropdown-toggle').text($(this).text() + ' ').append(a);
|
|
_socket.send($(this).val(), 'get_trial');
|
|
_socket.current_trial = $(this).val();
|
|
});
|
|
// Request first trial as default
|
|
_socket.send(0, 'get_trial')
|
|
_socket.current_trial = 0
|
|
};
|
|
|
|
var reset_trials = function() {
|
|
// 'Trials' selector
|
|
$('.dropdown-menu').empty();
|
|
var a = $('.dropdown-toggle .caret');
|
|
$('.dropdown-toggle').text('Trials ').append(a);
|
|
}
|
|
|
|
var convertJSON = function(json) {
|
|
// For NetworkX Geometric Graphs get positions
|
|
json.nodes.forEach(function(node) {
|
|
var scx = d3.scale.linear().domain([0, 1]).range([0, width]);
|
|
var scy = d3.scale.linear().domain([0, 1]).range([width, 0]);
|
|
if ( node.pos ) {
|
|
node.scx = scx(node.pos[0]);
|
|
node.scy = scy(node.pos[1]);
|
|
}
|
|
delete node.pos;
|
|
});
|
|
json.links.forEach(function(link) {
|
|
link.source = json.nodes[link.source]
|
|
link.target = json.nodes[link.target]
|
|
});
|
|
// Fix spells for nodes
|
|
json.nodes.forEach(function(node) {
|
|
for (i in node.spells) {
|
|
if (node.spells[i][0] > node.spells[i][1]) {
|
|
aux = node.spells[i][0];
|
|
node.spells[i][0] = node.spells[i][1];
|
|
node.spells[i][1] = aux;
|
|
}
|
|
}
|
|
});
|
|
return json;
|
|
}
|
|
|
|
var update_statistics_table = function() {
|
|
|
|
$('#percentTable tbody').empty()
|
|
|
|
var statisticsSorted = Object.keys(self.GraphVisualization.statistics).sort(function(a,b) {
|
|
return self.GraphVisualization.statistics[b] - self.GraphVisualization.statistics[a];
|
|
});
|
|
|
|
for ( var i in statisticsSorted ) {
|
|
if ( i <= 5 ) {
|
|
// Draw table
|
|
var appendTo = '#percentTable > tbody tr:nth-child(' + Number(parseInt(i) + 1) + ')';
|
|
var propertyName = (statisticsSorted[i].includes('class')) ?
|
|
statisticsSorted[i].split('.').pop().split('\'')[0] : statisticsSorted[i];
|
|
|
|
$('<tr>').addClass('col-sm-12').appendTo('#percentTable > tbody');
|
|
$('<td>').css('background-color', self.GraphVisualization.color($('.config-item #properties').val(), statisticsSorted[i])).addClass('col-sm-1').appendTo(appendTo);
|
|
$('<td>').addClass('text-left col-sm-4').text(self.GraphVisualization.statistics[statisticsSorted[i]] + ' %').appendTo(appendTo);
|
|
$('<td>').addClass('text-right col-sm-6 property-name').text(propertyName).appendTo(appendTo);
|
|
}
|
|
}
|
|
}
|
|
|
|
var set_configuration = function() {
|
|
// Number of nodes and links info table
|
|
$('<tr>').appendTo('#info-graph > tbody');
|
|
$('<th>').text('Nodes:').appendTo('#info-graph > tbody tr:nth-child(1)');
|
|
$('<th>').text(self.GraphVisualization.nodes).addClass('text-right').appendTo('#info-graph > tbody tr:nth-child(1)');
|
|
|
|
$('<tr>').appendTo('#info-graph > tbody');
|
|
$('<th>').text('Links:').appendTo('#info-graph > tbody tr:nth-child(2)');
|
|
$('<th>').text(self.GraphVisualization.links).addClass('text-right').appendTo('#info-graph > tbody tr:nth-child(2)');
|
|
|
|
// Options of 'Select'
|
|
for ( var i in self.GraphVisualization.model['dynamic'] ) {
|
|
$('<option>').val(self.GraphVisualization.model['dynamic'][i].title)
|
|
.text(self.GraphVisualization.model['dynamic'][i].title).appendTo('#properties-dynamic');
|
|
}
|
|
for ( var i in self.GraphVisualization.model['static'] ) {
|
|
$('<option>').val(self.GraphVisualization.model['static'][i].title)
|
|
.text(self.GraphVisualization.model['static'][i].title).appendTo('#properties-static');
|
|
}
|
|
|
|
// Hide optgroups if they are empty
|
|
if ( $('#properties-dynamic').children().length === 0 ) $('#properties-dynamic').hide();
|
|
if ( $('#properties-static').children().length === 0 ) $('#properties-static').hide();
|
|
|
|
update_statistics_table();
|
|
|
|
// Enable 'Link Distance' slider
|
|
$('#link-distance-slider').slider('enable').on('change', function(value) {
|
|
self.GraphVisualization.set_link_distance(value.value.newValue);
|
|
});
|
|
|
|
// Enable 'Run configuration' button
|
|
$('#run_simulation').attr('data-toggle', 'modal').attr('data-target', '#simulation_modal');
|
|
|
|
// Enable 'Download' buttons
|
|
$('#download_modal .btn-success').prop('disabled', false);
|
|
$('#download_gexf').on('click', function() {
|
|
_socket.send(_socket.current_trial, 'download_gexf')
|
|
});
|
|
$('#download_json').on('click', function() {
|
|
_socket.send(_socket.current_trial, 'download_json')
|
|
});
|
|
}
|
|
|
|
var reset_configuration = function() {
|
|
// Information table about the graph
|
|
$('#info-graph > tbody').empty();
|
|
|
|
// 'Select' for properties
|
|
$('#properties-dynamic').empty().show();
|
|
$('#properties-static').empty().show();
|
|
|
|
// 'Link Distance' slider
|
|
$('#link-distance-slider').slider('disable').slider('setValue', 30);
|
|
|
|
// 'Download' buttons
|
|
$('#download_gexf').off();
|
|
$('#download_json').off();
|
|
}
|
|
|
|
var slider;
|
|
|
|
var set_timeline = function(graph) {
|
|
// 'Timeline' slider
|
|
var [min, max] = get_limits(graph);
|
|
|
|
var stepUnix = 1;
|
|
var minUnix = (min !== Math.min()) ? min : 0;
|
|
var maxUnix = (max !== Math.max()) ? max : minUnix + 20;
|
|
|
|
slider = d3.slider();
|
|
d3.select('#slider3').attr('width', width).call(
|
|
slider.axis(true).min(minUnix).max(maxUnix).step(stepUnix).value(minUnix)
|
|
.on('slide', function(evt, value) {
|
|
self.GraphVisualization.update_graph($('.config-item #properties').val(), value, function() {
|
|
update_statistics_table();
|
|
});
|
|
})
|
|
);
|
|
|
|
// Draw graph for the first time
|
|
self.GraphVisualization.update_graph($('.config-item #properties').val(), maxUnix, function() {
|
|
update_statistics_table();
|
|
setTimeout(function() {
|
|
self.GraphVisualization.fit();
|
|
if ( $('svg #root > image').length !== 0 ) {
|
|
$('svg #root > image').attr('height', d3.select('#root').node().getBBox().height * 1.2);
|
|
var dx = d3.select('#graph-wrapper').node().getBBox().width - d3.select('svg #root > image').node().getBBox().width;
|
|
var dy = d3.select('#graph-wrapper').node().getBBox().height - d3.select('svg #root > image').node().getBBox().height;
|
|
$('svg #root > image').attr('transform', 'translate(' + (dx / 2) + ',' + (dy / 2) + ')');
|
|
$('svg #root > rect').attr('transform', 'translate(' + (dx / 2) + ',' + (dy / 2) + ')')
|
|
.attr('width', d3.select('svg #root > image').node().getBBox().width)
|
|
.attr('height', d3.select('svg #root > image').node().getBBox().height);
|
|
}
|
|
}, 1000);
|
|
});
|
|
|
|
// 'Speed' slider
|
|
$('#speed-slider').slider('enable').on('change', function(value) {
|
|
speed = value.value.newValue;
|
|
});
|
|
|
|
// Button 'Play'
|
|
$('button#button_play').on('click', function() {
|
|
play();
|
|
|
|
});
|
|
|
|
// Button 'Pause'
|
|
$('button#button_pause').on('click', function() {
|
|
stop();
|
|
$('button#button_play').removeClass('pressed').prop("disabled", false);
|
|
});
|
|
|
|
// Button 'Zoom to Fit'
|
|
$('button#button_zoomFit').click(function() { self.GraphVisualization.fit(); });
|
|
}
|
|
|
|
var player;
|
|
|
|
function play(){
|
|
$('button#button_play').addClass('pressed').prop("disabled", true);
|
|
|
|
if (slider.value() >= slider.max()) {
|
|
slider.value(slider.min());
|
|
}
|
|
|
|
var FRAME_INTERVAL = 100;
|
|
var speed_ratio = FRAME_INTERVAL / 1000 // speed=1 => 1 step per second
|
|
|
|
nextStep = function() {
|
|
newvalue = Math.min(slider.value() + speed*speed_ratio, slider.max());
|
|
console.log("new time value", newvalue);
|
|
slider.value(newvalue);
|
|
|
|
self.GraphVisualization.update_graph($('.config-item #properties').val(), slider.value(), function () {
|
|
update_statistics_table();
|
|
});
|
|
|
|
if (newvalue < slider.max()) {
|
|
player = setTimeout(nextStep, FRAME_INTERVAL);
|
|
} else {
|
|
$('button#button_play').removeClass('pressed').prop("disabled", false);
|
|
}
|
|
}
|
|
|
|
player = setTimeout(nextStep, FRAME_INTERVAL);
|
|
}
|
|
|
|
function stop() {
|
|
clearTimeout(player);
|
|
}
|
|
|
|
var reset_timeline = function() {
|
|
// 'Timeline' slider
|
|
$('#slider3').html('');
|
|
|
|
// 'Speed' slider
|
|
// $('#speed-slider').slider('disable').slider('setValue', 1000);
|
|
|
|
// Buttons
|
|
stop();
|
|
$('button#button_play').off().removeClass('pressed').prop("disabled", false);
|
|
$('button#button_pause').off();
|
|
$('button#button_zoomFit').off();
|
|
}
|
|
|
|
var get_limits = function(graph) {
|
|
var max = Math.max();
|
|
var min = Math.min()
|
|
graph.links.forEach(function(link) {
|
|
if (link.end > max) max = link.end
|
|
if (link.start > max) max = link.start
|
|
if (link.end < min) min = link.end
|
|
if (link.start < min) min = link.start
|
|
});
|
|
graph.nodes.forEach(function(node) {
|
|
for (property in node) {
|
|
if ( Array.isArray(node[property]) ) {
|
|
|
|
for (i in node[property]) {
|
|
for (j in node[property][i]) {
|
|
if (node[property][i][j] > max) max = node[property][i][j];
|
|
if (node[property][i][j] < min) min = node[property][i][j];
|
|
}
|
|
}
|
|
|
|
}
|
|
}
|
|
})
|
|
return [min, max];
|
|
}
|
|
|
|
var set_chart_nodes = function(graph, chart) {
|
|
var [min, max] = get_limits(graph);
|
|
var data = ['nodes']
|
|
for (var i = min; i <= max; i++) {
|
|
data.push(this.GraphVisualization.get_nodes(i));
|
|
}
|
|
chart.load({
|
|
unload: true,
|
|
columns: [data]
|
|
});
|
|
}
|
|
|
|
var set_chart_attrs = function(graph, chart, property) {
|
|
var [min, max] = get_limits(graph);
|
|
var data_tmp = {}
|
|
for (var i = min; i <= max; i++) {
|
|
this.GraphVisualization.get_attributes(property, i, function(object) {
|
|
for (var value in object) {
|
|
if (!data_tmp[value]) {
|
|
var time = 0
|
|
for (var done in data_tmp)
|
|
time = (data_tmp[done].length > time) ? data_tmp[done].length - 1 : time
|
|
data_tmp[value] = Array(time).fill(0);
|
|
}
|
|
data_tmp[value].push(object[value]);
|
|
}
|
|
});
|
|
}
|
|
var data = $.map(data_tmp, function(value, index) {
|
|
value.splice(0,0,index);
|
|
return [value];
|
|
});
|
|
chart.load({
|
|
unload: true,
|
|
columns: data
|
|
});
|
|
chart.axis.labels({y: property});
|
|
}
|
|
|
|
var create_chart = function(width, height, label_x, label_y, bind_to) {
|
|
return c3.generate({
|
|
size: {
|
|
width: width,
|
|
height: height
|
|
},
|
|
data: {
|
|
columns: [],
|
|
type: 'area-spline'
|
|
},
|
|
axis: {
|
|
x: { label: label_x },
|
|
y: { label: label_y }
|
|
},
|
|
point: { show: false },
|
|
bindto: bind_to
|
|
});
|
|
}
|
|
|
|
var run_simulation = function() {
|
|
var environment_variables = {}
|
|
$('#wrapper-settings input').each(function() {
|
|
switch(this.type) {
|
|
case 'text':
|
|
environment_variables[this.id] = Number(this.value);
|
|
break;
|
|
case 'checkbox':
|
|
environment_variables[this.id] = ($(this).is(':checked')) ? true : false;
|
|
break;
|
|
case 'number':
|
|
environment_variables[this.id] = Number(this.value);
|
|
break;
|
|
default:
|
|
console.warn(this.id + ' not defined when running simulation!');
|
|
break;
|
|
}
|
|
|
|
});
|
|
return environment_variables;
|
|
}
|
|
|
|
var download = function(filename, filetype, content) {
|
|
var file = document.createElement('a');
|
|
file.setAttribute('href', 'data:text/' + filetype + ';charset=utf-8,' + encodeURIComponent(content));
|
|
file.setAttribute('download', filename);
|
|
file.click();
|
|
delete file;
|
|
}
|