From 6f8cc9ba50798f067aebef9f7fe4f50e4e1005db Mon Sep 17 00:00:00 2001 From: Tasio Mendez Date: Fri, 15 Dec 2017 17:59:50 +0100 Subject: [PATCH] Python Server --- .gitignore | 2 + config.yml | 25 ++ run.py | 15 + server.py | 159 +++++++++ templates/css/main.css | 238 +++++++++++++ templates/css/timeline.css | 72 ++++ templates/img/logo_gsi.svg | 140 ++++++++ templates/index.html | 226 ++++++++++++ templates/js/socket.js | 269 +++++++++++++++ templates/js/timeline.js | 429 +++++++++++++++++++++++ templates/js/visualization.js | 426 +++++++++++++++++++++++ templates/js_old/helper.js | 20 ++ templates/js_old/initial.js | 561 ++++++++++++++++++++++++++++++ templates/js_old/parser.js | 622 ++++++++++++++++++++++++++++++++++ visualization.py | 67 ++++ 15 files changed, 3271 insertions(+) create mode 100644 .gitignore create mode 100644 config.yml create mode 100644 run.py create mode 100644 server.py create mode 100644 templates/css/main.css create mode 100644 templates/css/timeline.css create mode 100644 templates/img/logo_gsi.svg create mode 100644 templates/index.html create mode 100755 templates/js/socket.js create mode 100644 templates/js/timeline.js create mode 100644 templates/js/visualization.js create mode 100644 templates/js_old/helper.js create mode 100644 templates/js_old/initial.js create mode 100644 templates/js_old/parser.js create mode 100644 visualization.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..bacca8e --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +__pycache__/ +output/ diff --git a/config.yml b/config.yml new file mode 100644 index 0000000..1f741eb --- /dev/null +++ b/config.yml @@ -0,0 +1,25 @@ +name: ControlModelM2_sim +max_time: 50 +num_trials: 2 +network_params: + generator: barabasi_albert_graph + n: 100 + m: 2 +network_agents: + - agent_type: ControlModelM2 + weight: 0.1 + state: + id: 1 + - agent_type: ControlModelM2 + weight: 0.9 + state: + id: 0 +environment_params: + prob_neutral_making_denier: 0.035 + prob_infect: 0.075 + prob_cured_healing_infected: 0.035 + prob_cured_vaccinate_neutral: 0.035 + prob_vaccinated_healing_infected: 0.035 + prob_vaccinated_vaccinate_neutral: 0.035 + prob_generate_anti_rumor: 0.035 + standard_variance: 0.055 diff --git a/run.py b/run.py new file mode 100644 index 0000000..dd3abdd --- /dev/null +++ b/run.py @@ -0,0 +1,15 @@ + +from server import ModularServer +from visualization import GraphVisualization, Model + + +def run(model, params=None): + graphVisualization = GraphVisualization(params) + server = ModularServer(model, graphVisualization, name="SOIL Model") + server.port = 8001 + server.launch() + + +if __name__ == "__main__": + soil = Model(dump=False) + run(soil) diff --git a/server.py b/server.py new file mode 100644 index 0000000..7ca841c --- /dev/null +++ b/server.py @@ -0,0 +1,159 @@ +import os +import tornado.ioloop +import tornado.web +import tornado.websocket +import tornado.escape +import tornado.gen +import webbrowser + +import yaml +import logging + +logger = logging.getLogger(__name__) +logger.setLevel(logging.INFO) + +class VisualizationElement: + """ + Defines an element of the visualization. + Attributes: + package_includes: A list of external JavaScript files to include that + are part of the packages. + local_includes: A list of JavaScript files that are local to the + directory that the server is being run in. + js_code: A JavaScript code string to instantiate the element. + Methods: + render: Takes a model object, and produces JSON data which can be sent + to the client. + """ + + package_includes = [] + local_includes = [] + js_code = '' + render_args = {} + + def __init__(self): + pass + + def render(self, model): + return 'VisualizationElement goes here.' + + +class PageHandler(tornado.web.RequestHandler): + """ Handler for the HTML template which holds the visualization. """ + + def get(self): + self.render('index.html', port=self.application.port, + model_name=self.application.model_name, + package_includes=self.application.package_includes, + local_includes=self.application.local_includes, + scripts=self.application.js_code) + + +class SocketHandler(tornado.websocket.WebSocketHandler): + + def open(self): + if self.application.verbose: + logger.info('Socket opened!') + + def check_origin(self, origin): + return True + + def on_message(self, message): + """ Receiving a message from the websocket, parse, and act accordingly. """ + + msg = tornado.escape.json_decode(message) + + if msg['type'] == 'config_file': + + if self.application.verbose: + print(msg['data']) + + config = list(yaml.load_all(msg['data'])) + + if len(config) > 1: + error = 'Please, provide only one configuration.' + if self.application.verbose: + logger.error(error) + self.write_message({'type': 'error', + 'error': error}) + return + else: + config = config[0] + + self.name = config['name'] + self.application.model.run(config) + + trials = [] + for i in range(config['num_trials']): + trials.append('{}_trial_{}'.format(self.name, i)) + self.write_message({'type': 'trials', + 'data': trials }) + + elif msg['type'] == 'get_trial': + if self.application.verbose: + logger.info('Trial {} requested!'.format(msg['data'])) + self.write_message({'type': 'get_trial', + 'data': self.application.model.get_trial(self.name, msg['data']) }) + + else: + if self.application.verbose: + logger.info('Unexpected message!') + + +class ModularServer(tornado.web.Application): + """ Main visualization application. """ + + portrayal_method = None + port = 8001 + model_args = () + model_kwargs = {} + page_handler = (r'/', PageHandler) + socket_handler = (r'/ws', SocketHandler) + static_handler = (r'/(.*)', tornado.web.StaticFileHandler, + # {'path': os.path.dirname(__file__)}) + {'path': 'templates'}) + local_handler = (r'/local/(.*)', tornado.web.StaticFileHandler, + {'path': ''}) + + handlers = [page_handler, socket_handler, static_handler, local_handler] + settings = {'debug': True, + 'template_path': os.path.dirname(__file__) + '/templates'} + + def __init__(self, model, visualization_element, name='SOIL Model', verbose=True, + *args, **kwargs): + + self.verbose = verbose + self.package_includes = set() + self.local_includes = set() + self.js_code = [] + + self.visualization_element = visualization_element + + self.model_name = name + self.model = model + self.model_args = args + self.model_kwargs = kwargs + + #self.reset_model() + + # Initializing the application itself: + super().__init__(self.handlers, **self.settings) + + ''' + def reset_model(self): + self.model = self.model_cls(*self.model_args, **self.model_kwargs) + ''' + + def render_model(self): + return self.visualization_element.render(self.model) + + def launch(self, port=None): + """ Run the app. """ + + if port is not None: + self.port = port + url = 'http://127.0.0.1:{PORT}'.format(PORT=self.port) + print('Interface starting at {url}'.format(url=url)) + self.listen(self.port) + webbrowser.open(url) + tornado.ioloop.IOLoop.instance().start() diff --git a/templates/css/main.css b/templates/css/main.css new file mode 100644 index 0000000..75abcee --- /dev/null +++ b/templates/css/main.css @@ -0,0 +1,238 @@ + +.node { + stroke: #fff; + stroke-width: 1.5px; +} + +.link { + stroke: #999; + stroke-opacity: .6; +} + +svg#graph, #configuration { + background-color: white; + margin-top: 15px; + border-style: double; + border-color: rgba(0, 0, 0, 0.35); + border-radius: 5px; + padding: 0px; +} + +#timeline { + padding: 0; + margin-top: 20px; +} + +#configuration { + margin-top: 15px; + padding: 15px; + border-left: none !important; + overflow: auto; +} + +button { + outline: none !important; +} + +.btn-toolbar.controls { + position: absolute; + right: 0; +} + +.controls > .btn { + margin-left: 10px !important; +} + +button.pressed { + background-color: rgb(167, 242, 168); + -webkit-animation: background 1s cubic-bezier(1,0,0,1) infinite; + animation: background 1s cubic-bezier(1,0,0,1) infinite; + cursor: default !important; +} + +@-webkit-keyframes background { + 50% { background-color: #dddddd; } + 100% { background-color: rgb(167, 242, 168); } +} + +@keyframes background { + 50% { background-color: #dddddd; } + 100% { background-color: rgb(167, 242, 168); } +} + +#slider3 { + background: repeating-linear-gradient( 90deg, white 27px, white 30px, #fff 32px, #aaa 33px ); + background-color: white; +} + +.config-item { + margin-top: 15px !important; +} + +/** LOADER **/ +#load { + position: absolute; + font-weight: bold; +} + +#load.loader { + border: 5px solid #f3f3f3; + border-radius: 50%; + border-top: 5px solid #3498db; + border-bottom: 5px solid #3498db; + width: 30px; + height: 30px; + -webkit-animation: spin 1s linear infinite; + animation: spin 1s linear infinite; + position: absolute; +} + +#load:before { + content: 'No file' +} + +#load.loader:before { + content: '' !important; +} + +@-webkit-keyframes spin { + 0% { -webkit-transform: rotate(0deg); } + 100% { -webkit-transform: rotate(360deg); } +} + +@keyframes spin { + 0% { transform: rotate(0deg); } + 100% { transform: rotate(360deg); } +} + +/** ALERT **/ +.alert-danger { + position: absolute; + margin-top: 20px; + margin-left: 5px; +} + +/** FILE BROWSER **/ +.custom-file { + position: relative; + display: inline-block; + width: 100%; + height: 35px; + margin-bottom: 0; + cursor: pointer; +} + +.custom-file-input { + min-width: 14rem; + max-width: 100%; + height: 35px; + margin: 0; + filter: alpha(opacity=0); + opacity: 0; +} + +.custom-file-control { + position: absolute; + top: 0; + right: 0; + left: 0; + z-index: 5; + height: 35px; + padding: .5rem 1rem; + line-height: 1.5; + color: #464a4c; + pointer-events: none; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + background-color: #fff; + border: 1px solid rgba(0,0,0,.15); + border-radius: .25rem; +} + +.custom-file-control::before { + content: "Browse"; + position: absolute; + top: -1px; + right: -1px; + bottom: -1px; + z-index: 6; + display: block; + height: 35px; + padding: .5rem 1rem; + line-height: 1.5; + color: #464a4c; + background-color: #eceeef; + border: 1px solid rgba(0,0,0,.15); + border-radius: 0 .25rem .25rem 0; +} + +.custom-file-control::after { + content: attr(data-content); +} + +/** TABLES **/ +#percentTable { + height: 150px !important; + width: 100% !important; +} + +#percentTable tr { + padding: 5px 2px; +} + +hr { + margin-top: 15px !important; + margin-bottom: 15px !important; +} + +#info-graph { + width: 70% !important; +} + +.logo { + margin-top: -40px; + position: absolute; + right: 15px; +} + +/** SLIDER **/ +.speed-slider, +.link-distance-slider { + padding: 0 10px !important; + margin-top: 5px !important; + width: 100% !important; +} + +.speed-slider .slider, +.link-distance-slider .slider { + width: 100% !important; +} + +.speed-slider .slider-selection, +.link-distance-slider .slider-selection { + background-image: linear-gradient(to bottom, + rgba(36, 110, 162, 0.5) 0%, + rgba(3, 169, 224, 0.5) 100%) !important; +} + +.slider-disabled .slider-selection { + opacity: 0.5; +} + +.slider.slider-disabled .slider-track { + cursor: default !important; +} + +table#speed, +table#link-distance { + width: 100%; +} + +table#speed .min, +table#speed .max, +table#link-distance .min, +table#link-distance .max { + font-weight: normal !important; +} diff --git a/templates/css/timeline.css b/templates/css/timeline.css new file mode 100644 index 0000000..d6d042d --- /dev/null +++ b/templates/css/timeline.css @@ -0,0 +1,72 @@ +#slider3 { + margin: 0 0 10px 0; +} + +.d3-slider { + position: relative; + font-family: Verdana,Arial,sans-serif; + font-size: 1.1em; + border: 1px solid #aaaaaa; + z-index: 2; +} + +.d3-slider-horizontal { + height: 40px; +} + +.d3-slider-range { + background:#2980b9; + left:0px; + right:0px; + height: 0.8em; + position: absolute; +} + +.d3-slider-handle { + position: absolute; + width: .8em; + height: 48px; + border: 1px solid #d3d3d3; + border-radius: 4px; + background: #eee; + background: linear-gradient(to bottom, #eee 0%, #ddd 100%); + z-index: 3; +} + +.d3-slider-handle:hover { + border: 1px solid #999999; +} + +.d3-slider-horizontal .d3-slider-handle { + top: -.3em; + margin-left: -.4em; +} + +.d3-slider-axis { + position: relative; + z-index: 1; +} + +.d3-slider-axis-bottom { + top: 38px; +} + +.d3-slider-axis-right { + left: .8em; +} + +.d3-slider-axis path { + stroke-width: 0; + fill: none; +} + +.d3-slider-axis line { + fill: none; + stroke: #aaa; + shape-rendering: crispEdges; + stroke-dasharray: 2; +} + +.d3-slider-axis text { + font-size: 11px; +} diff --git a/templates/img/logo_gsi.svg b/templates/img/logo_gsi.svg new file mode 100644 index 0000000..522fb60 --- /dev/null +++ b/templates/img/logo_gsi.svg @@ -0,0 +1,140 @@ + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/templates/index.html b/templates/index.html new file mode 100644 index 0000000..860177d --- /dev/null +++ b/templates/index.html @@ -0,0 +1,226 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {{ model_name }} + + + + + + + +
+ +
+ +
+ +
+ +
+ +
+
+ + + +
+ Trials: + +
+
+ + + +
+ + + + + +
Nodes:
+ +
+
+ + + +
+ Propiedades: + +
+
+
+ + +
+
+
+ + + +
+ + + + +
minSpeedmax
+
+ +
+
+
+ + + + +
+ + + + + + +
+ + +
+ + +
+
+ +
+ + + +
+
+ + + + + + +
+ + + \ No newline at end of file diff --git a/templates/js/socket.js b/templates/js/socket.js new file mode 100755 index 0000000..dab0193 --- /dev/null +++ b/templates/js/socket.js @@ -0,0 +1,269 @@ + +// 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': + $('#load').removeClass('loader'); + set_trials(msg['data']); + break; + + case 'get_trial': + console.log(msg['data']); + GraphVisualization.import(convertJSON(msg['data']['graph']), msg['data']['models'], function() { + $('#load').hide(); + reset_configuration(); + set_configuration(); + reset_timeline(); + set_timeline(msg['data']['graph']); + }); + break; + + case 'error': + console.log(msg['error']); + _socket.error(msg['error']); + $('#load').removeClass('loader'); + + default: + console.log('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(); + } +}; + +var set_trials = function(trials) { + for ( i in trials ) { + $('