diff --git a/soil/web/.gitignore b/soil/web/.gitignore new file mode 100644 index 0000000..a9905b9 --- /dev/null +++ b/soil/web/.gitignore @@ -0,0 +1,4 @@ +__pycache__/ +output/ +tests/ +soil_output/ diff --git a/soil/web/README.md b/soil/web/README.md new file mode 100644 index 0000000..79f5b42 --- /dev/null +++ b/soil/web/README.md @@ -0,0 +1,59 @@ +# Graph Visualization with D3.js + +The aim of this software is to provide a useful tool for visualising and analysing the result of different simulations based on graph. Once you run the simulation, you will be able to interact with the simulation in real time. + +For this purpose, a model which tries to simulate the spread of information to comprehend the radicalism spread in a society is included. Whith all this, the main project goals could be divided in five as it is shown in the following. + +* Simulate the spread of information through a network applied to radicalism. +* Visualize the results of the simulation. +* Interact with the simulation in real time. +* Extract data from the results. +* Show data in a right way for its research. + +## Deploying the server + +For deploying the application, you will only need to run the following command. + +`python3 run.py [--name NAME] [--dump] [--port PORT] [--verbose]` + +Where the options are detailed in the following table. + +| Option | Description | +| --- | --- | +| `--name NAME` | The name of the simulation. It will appear on the app. | +| `--dump` | For dumping the results in server side. | +| `--port PORT` | The port where the server will listen. | +| `--verbose` | Verbose mode. | + +> You can dump the results of the simulation in server side. Anyway, you will be able to download them in GEXF or JSON Graph format directly from the browser. + +## Visualization Params + +The configuration of the simulation is based on the simulator configuration. In this case, it follows the [SOIL](https://github.com/gsi-upm/soil) configuration syntax and for visualising the results in a more comfortable way, more params can be added in `visualization_params` dictionary. + +* For setting a background image, the tag needed is `background image`. You can also add a `background_opacity` and `background_filter_color` if the image is so clear than you can difficult view the nodes. +* For setting colors to the nodes, you can do it based on their properties values. Using the `color` tag, you will need to indicate the attribute key and value, and then the color you want to apply. +* The shapes applied to a group of nodes are always the same. This means than it won't change dynamically, so you will have to indicate the property with the `shape_property` tag and add a dictionary called `shapes` in which for each value, you indicate the shape. + All shapes have to had been downloaded before in SVG format and added to the server. + +An example of this configuration applied to the TerroristNetworkModel is presented. + +```yaml +visualization_params: + # Icons downloaded from https://www.iconfinder.com/ + shape_property: agent + shapes: + TrainingAreaModel: target + HavenModel: home + TerroristNetworkModel: person + colors: + - attr_id: 0 + color: '#40de40' + - attr_id: 1 + color: red + - attr_id: 2 + color: '#c16a6a' + background_image: 'map_4800x2860.jpg' + background_opacity: '0.9' + background_filter_color: 'blue' +``` diff --git a/soil/web/TerroristNetworkModel.py b/soil/web/TerroristNetworkModel.py new file mode 100644 index 0000000..d81ff7f --- /dev/null +++ b/soil/web/TerroristNetworkModel.py @@ -0,0 +1,255 @@ +import random +import networkx as nx +from soil.agents import BaseAgent, FSM, state, default_state +from scipy.spatial import cKDTree as KDTree + +global betweenness_centrality_global +global degree_centrality_global + +betweenness_centrality_global = None +degree_centrality_global = None + +class TerroristSpreadModel(FSM): + """ + Settings: + information_spread_intensity + + terrorist_additional_influence + + min_vulnerability (optional else zero) + + max_vulnerability + + prob_interaction + """ + + def __init__(self, environment=None, agent_id=0, state=()): + super().__init__(environment=environment, agent_id=agent_id, state=state) + + global betweenness_centrality_global + global degree_centrality_global + + if betweenness_centrality_global == None: + betweenness_centrality_global = nx.betweenness_centrality(self.global_topology) + if degree_centrality_global == None: + degree_centrality_global = nx.degree_centrality(self.global_topology) + + self.information_spread_intensity = environment.environment_params['information_spread_intensity'] + self.terrorist_additional_influence = environment.environment_params['terrorist_additional_influence'] + self.prob_interaction = environment.environment_params['prob_interaction'] + + if self['id'] == self.civilian.id: # Civilian + self.initial_belief = random.uniform(0.00, 0.5) + elif self['id'] == self.terrorist.id: # Terrorist + self.initial_belief = random.uniform(0.8, 1.00) + elif self['id'] == self.leader.id: # Leader + self.initial_belief = 1.00 + else: + raise Exception('Invalid state id: {}'.format(self['id'])) + + if 'min_vulnerability' in environment.environment_params: + self.vulnerability = random.uniform( environment.environment_params['min_vulnerability'], environment.environment_params['max_vulnerability'] ) + else : + self.vulnerability = random.uniform( 0, environment.environment_params['max_vulnerability'] ) + + self.mean_belief = self.initial_belief + self.betweenness_centrality = betweenness_centrality_global[self.id] + self.degree_centrality = degree_centrality_global[self.id] + + # self.state['radicalism'] = self.mean_belief + + def count_neighboring_agents(self, state_id=None): + if isinstance(state_id, list): + return len(self.get_neighboring_agents(state_id)) + else: + return len(super().get_agents(state_id, limit_neighbors=True)) + + def get_neighboring_agents(self, state_id=None): + if isinstance(state_id, list): + _list = [] + for i in state_id: + _list += super().get_agents(i, limit_neighbors=True) + return [ neighbour for neighbour in _list if isinstance(neighbour, TerroristSpreadModel) ] + else: + _list = super().get_agents(state_id, limit_neighbors=True) + return [ neighbour for neighbour in _list if isinstance(neighbour, TerroristSpreadModel) ] + + @state + def civilian(self): + if self.count_neighboring_agents() > 0: + neighbours = [] + for neighbour in self.get_neighboring_agents(): + if random.random() < self.prob_interaction: + neighbours.append(neighbour) + influence = sum( neighbour.degree_centrality for neighbour in neighbours ) + mean_belief = sum( neighbour.mean_belief * neighbour.degree_centrality / influence for neighbour in neighbours ) + self.initial_belief = self.mean_belief + mean_belief = mean_belief * self.information_spread_intensity + self.initial_belief * ( 1 - self.information_spread_intensity ) + self.mean_belief = mean_belief * self.vulnerability + self.initial_belief * ( 1 - self.vulnerability ) + + if self.mean_belief >= 0.8: + return self.terrorist + + @state + def leader(self): + self.mean_belief = self.mean_belief ** ( 1 - self.terrorist_additional_influence ) + if self.count_neighboring_agents(state_id=[self.terrorist.id, self.leader.id]) > 0: + for neighbour in self.get_neighboring_agents(state_id=[self.terrorist.id, self.leader.id]): + if neighbour.betweenness_centrality > self.betweenness_centrality: + return self.terrorist + + @state + def terrorist(self): + if self.count_neighboring_agents(state_id=[self.terrorist.id, self.leader.id]) > 0: + neighbours = self.get_neighboring_agents(state_id=[self.terrorist.id, self.leader.id]) + influence = sum( neighbour.degree_centrality for neighbour in neighbours ) + mean_belief = sum( neighbour.mean_belief * neighbour.degree_centrality / influence for neighbour in neighbours ) + self.initial_belief = self.mean_belief + self.mean_belief = mean_belief * self.vulnerability + self.initial_belief * ( 1 - self.vulnerability ) + self.mean_belief = self.mean_belief ** ( 1 - self.terrorist_additional_influence ) + + if self.count_neighboring_agents(state_id=self.leader.id) == 0 and self.count_neighboring_agents(state_id=self.terrorist.id) > 0: + max_betweenness_centrality = self + for neighbour in self.get_neighboring_agents(state_id=self.terrorist.id): + if neighbour.betweenness_centrality > max_betweenness_centrality.betweenness_centrality: + max_betweenness_centrality = neighbour + if max_betweenness_centrality == self: + return self.leader + + def add_edge(self, G, source, target): + G.add_edge(source.id, target.id, start=self.env._now) + + def link_search(self, G, node, radius): + pos = nx.get_node_attributes(G, 'pos') + nodes, coords = list(zip(*pos.items())) + kdtree = KDTree(coords) # Cannot provide generator. + edge_indexes = kdtree.query_pairs(radius, 2) + _list = [ edge[int(not edge.index(node))] for edge in edge_indexes if node in edge ] + return [ G.nodes()[index]['agent'] for index in _list ] + + def social_search(self, G, node, steps): + nodes = list(nx.ego_graph(G, node, radius=steps).nodes()) + nodes.remove(node) + return [ G.nodes()[index]['agent'] for index in nodes ] + + +class TrainingAreaModel(FSM): + """ + Settings: + training_influence + + min_vulnerability + + Requires TerroristSpreadModel. + """ + + def __init__(self, environment=None, agent_id=0, state=()): + super().__init__(environment=environment, agent_id=agent_id, state=state) + self.training_influence = environment.environment_params['training_influence'] + if 'min_vulnerability' in environment.environment_params: + self.min_vulnerability = environment.environment_params['min_vulnerability'] + else: self.min_vulnerability = 0 + + @default_state + @state + def terrorist(self): + for neighbour in self.get_neighboring_agents(): + if isinstance(neighbour, TerroristSpreadModel) and neighbour.vulnerability > self.min_vulnerability: + neighbour.vulnerability = neighbour.vulnerability ** ( 1 - self.training_influence ) + + +class HavenModel(FSM): + """ + Settings: + haven_influence + + min_vulnerability + + max_vulnerability + + Requires TerroristSpreadModel. + """ + + def __init__(self, environment=None, agent_id=0, state=()): + super().__init__(environment=environment, agent_id=agent_id, state=state) + self.haven_influence = environment.environment_params['haven_influence'] + if 'min_vulnerability' in environment.environment_params: + self.min_vulnerability = environment.environment_params['min_vulnerability'] + else: self.min_vulnerability = 0 + self.max_vulnerability = environment.environment_params['max_vulnerability'] + + @state + def civilian(self): + for neighbour_agent in self.get_neighboring_agents(): + if isinstance(neighbour_agent, TerroristSpreadModel) and neighbour_agent['id'] == neighbour_agent.civilian.id: + for neighbour in self.get_neighboring_agents(): + if isinstance(neighbour, TerroristSpreadModel) and neighbour.vulnerability > self.min_vulnerability: + neighbour.vulnerability = neighbour.vulnerability * ( 1 - self.haven_influence ) + return self.civilian + return self.terrorist + + @state + def terrorist(self): + for neighbour in self.get_neighboring_agents(): + if isinstance(neighbour, TerroristSpreadModel) and neighbour.vulnerability < self.max_vulnerability: + neighbour.vulnerability = neighbour.vulnerability ** ( 1 - self.haven_influence ) + return self.terrorist + + +class TerroristNetworkModel(TerroristSpreadModel): + """ + Settings: + sphere_influence + + vision_range + + weight_social_distance + + weight_link_distance + """ + + def __init__(self, environment=None, agent_id=0, state=()): + super().__init__(environment=environment, agent_id=agent_id, state=state) + + self.vision_range = environment.environment_params['vision_range'] + self.sphere_influence = environment.environment_params['sphere_influence'] + self.weight_social_distance = environment.environment_params['weight_social_distance'] + self.weight_link_distance = environment.environment_params['weight_link_distance'] + + @state + def terrorist(self): + self.update_relationships() + return super().terrorist() + + @state + def leader(self): + self.update_relationships() + return super().leader() + + def update_relationships(self): + if self.count_neighboring_agents(state_id=self.civilian.id) == 0: + close_ups = self.link_search(self.global_topology, self.id, self.vision_range) + step_neighbours = self.social_search(self.global_topology, self.id, self.sphere_influence) + search = list(set(close_ups).union(step_neighbours)) + neighbours = self.get_neighboring_agents() + search = [item for item in search if not item in neighbours and isinstance(item, TerroristNetworkModel)] + for agent in search: + social_distance = 1 / self.shortest_path_length(self.global_topology, self.id, agent.id) + spatial_proximity = ( 1 - self.get_distance(self.global_topology, self.id, agent.id) ) + prob_new_interaction = self.weight_social_distance * social_distance + self.weight_link_distance * spatial_proximity + if agent['id'] == agent.civilian.id and random.random() < prob_new_interaction: + self.add_edge(self.global_topology, self, agent) + break + + def get_distance(self, G, source, target): + source_x, source_y = nx.get_node_attributes(G, 'pos')[source] + target_x, target_y = nx.get_node_attributes(G, 'pos')[target] + dx = abs( source_x - target_x ) + dy = abs( source_y - target_y ) + return ( dx ** 2 + dy ** 2 ) ** ( 1 / 2 ) + + def shortest_path_length(self, G, source, target): + try: + return nx.shortest_path_length(G, source, target) + except nx.NetworkXNoPath: + return float('inf') diff --git a/soil/web/TerroristNetworkModel.yml b/soil/web/TerroristNetworkModel.yml new file mode 100644 index 0000000..b42b06d --- /dev/null +++ b/soil/web/TerroristNetworkModel.yml @@ -0,0 +1,62 @@ +name: TerroristNetworkModel_sim +load_module: TerroristNetworkModel +max_time: 150 +num_trials: 1 +network_params: + generator: random_geometric_graph + radius: 0.2 + # generator: geographical_threshold_graph + # theta: 20 + n: 100 +network_agents: + - agent_type: TerroristNetworkModel + weight: 0.8 + state: + id: civilian # Civilians + - agent_type: TerroristNetworkModel + weight: 0.1 + state: + id: leader # Leaders + - agent_type: TrainingAreaModel + weight: 0.05 + state: + id: terrorist # Terrorism + - agent_type: HavenModel + weight: 0.05 + state: + id: civilian # Civilian + +environment_params: + # TerroristSpreadModel + information_spread_intensity: 0.7 + terrorist_additional_influence: 0.035 + max_vulnerability: 0.7 + prob_interaction: 0.5 + + # TrainingAreaModel and HavenModel + training_influence: 0.20 + haven_influence: 0.20 + + # TerroristNetworkModel + vision_range: 0.30 + sphere_influence: 2 + weight_social_distance: 0.035 + weight_link_distance: 0.035 + +visualization_params: + # Icons downloaded from https://www.iconfinder.com/ + shape_property: agent + shapes: + TrainingAreaModel: target + HavenModel: home + TerroristNetworkModel: person + colors: + - attr_id: civilian + color: '#40de40' + - attr_id: terrorist + color: red + - attr_id: leader + color: '#c16a6a' + background_image: 'map_4800x2860.jpg' + background_opacity: '0.9' + background_filter_color: 'blue' diff --git a/soil/web/config.yml b/soil/web/config.yml new file mode 100644 index 0000000..1f741eb --- /dev/null +++ b/soil/web/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/soil/web/run.py b/soil/web/run.py new file mode 100644 index 0000000..a0b1416 --- /dev/null +++ b/soil/web/run.py @@ -0,0 +1,23 @@ +import argparse +from server import ModularServer +from simulator import Simulator + + +def run(simulator, name="SOIL", port=8001, verbose=False): + server = ModularServer(simulator, name=(name[0] if isinstance(name, list) else name), verbose=verbose) + server.port = port + server.launch() + + +if __name__ == "__main__": + + parser = argparse.ArgumentParser(description='Visualization of a Graph Model') + + parser.add_argument('--name', '-n', nargs=1, default='SOIL', help='name of the simulation') + parser.add_argument('--dump', '-d', help='dumping results in folder output', action='store_true') + parser.add_argument('--port', '-p', nargs=1, default=8001, help='port for launching the server') + parser.add_argument('--verbose', '-v', help='verbose mode', action='store_true') + args = parser.parse_args() + + soil = Simulator(dump=args.dump) + run(soil, name=args.name, port=(args.port[0] if isinstance(args.port, list) else args.port), verbose=args.verbose) diff --git a/soil/web/server.py b/soil/web/server.py new file mode 100644 index 0000000..e6d2695 --- /dev/null +++ b/soil/web/server.py @@ -0,0 +1,222 @@ +import io +import logging +import networkx as nx +import os +import threading +import tornado.ioloop +import tornado.web +import tornado.websocket +import tornado.escape +import tornado.gen +import yaml +import webbrowser +from contextlib import contextmanager +from time import sleep +from xml.etree.ElementTree import tostring + +logger = logging.getLogger(__name__) +logger.setLevel(logging.INFO) + + +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, + name=self.application.name) + + +class SocketHandler(tornado.websocket.WebSocketHandler): + """ Handler for websocket. """ + + 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']) + + self.config = list(yaml.load_all(msg['data'])) + + if len(self.config) > 1: + error = 'Please, provide only one configuration.' + if self.application.verbose: + logger.error(error) + self.write_message({'type': 'error', + 'error': error}) + return + else: + self.config = self.config[0] + self.send_log('INFO.' + self.application.simulator.name, 'Using config: {name}'.format(name=self.config['name'])) + + if 'visualization_params' in self.config: + self.write_message({'type': 'visualization_params', + 'data': self.config['visualization_params']}) + self.name = self.config['name'] + self.run_simulation() + + settings = [] + for key in self.config['environment_params']: + if type(self.config['environment_params'][key]) == float or type(self.config['environment_params'][key]) == int: + if self.config['environment_params'][key] <= 1: + setting_type = 'number' + else: + setting_type = 'great_number' + elif type(self.config['environment_params'][key]) == bool: + setting_type = 'boolean' + else: + setting_type = 'undefined' + + settings.append({ + 'label': key, + 'type': setting_type, + 'value': self.config['environment_params'][key] + }) + + self.write_message({'type': 'settings', + 'data': settings}) + + elif msg['type'] == 'get_trial': + if self.application.verbose: + logger.info('Trial {} requested!'.format(msg['data'])) + self.send_log('INFO.' + __name__, 'Trial {} requested!'.format(msg['data'])) + self.write_message({'type': 'get_trial', + 'data': self.get_trial( int(msg['data']) ) }) + + elif msg['type'] == 'run_simulation': + if self.application.verbose: + logger.info('Running new simulation for {name}'.format(name=self.config['name'])) + self.send_log('INFO.' + self.application.simulator.name, 'Running new simulation for {name}'.format(name=self.config['name'])) + self.config['environment_params'] = msg['data'] + self.run_simulation() + + elif msg['type'] == 'download_gexf': + G = self.simulation[ int(msg['data']) ].history_to_graph() + for node in G.nodes(): + if 'pos' in G.node[node]: + G.node[node]['viz'] = {"position": {"x": G.node[node]['pos'][0], "y": G.node[node]['pos'][1], "z": 0.0}} + del (G.node[node]['pos']) + writer = nx.readwrite.gexf.GEXFWriter(version='1.2draft') + writer.add_graph(G) + self.write_message({'type': 'download_gexf', + 'filename': self.config['name'] + '_trial_' + str(msg['data']), + 'data': tostring(writer.xml).decode(writer.encoding) }) + + elif msg['type'] == 'download_json': + G = self.simulation[ int(msg['data']) ].history_to_graph() + for node in G.nodes(): + if 'pos' in G.node[node]: + G.node[node]['viz'] = {"position": {"x": G.node[node]['pos'][0], "y": G.node[node]['pos'][1], "z": 0.0}} + del (G.node[node]['pos']) + self.write_message({'type': 'download_json', + 'filename': self.config['name'] + '_trial_' + str(msg['data']), + 'data': nx.node_link_data(G) }) + + else: + if self.application.verbose: + logger.info('Unexpected message!') + + def update_logging(self): + try: + if (not self.log_capture_string.closed and self.log_capture_string.getvalue()): + for i in range(len(self.log_capture_string.getvalue().split('\n')) - 1): + self.send_log('INFO.' + self.application.simulator.name, self.log_capture_string.getvalue().split('\n')[i]) + self.log_capture_string.truncate(0) + self.log_capture_string.seek(0) + finally: + if self.capture_logging: + thread = threading.Timer(0.01, self.update_logging) + thread.start() + + def on_close(self): + if self.application.verbose: + logger.info('Socket closed!') + + def send_log(self, logger, logging): + self.write_message({'type': 'log', + 'logger': logger, + 'logging': logging }) + + def run_simulation(self): + # Run simulation and capture logs + if 'visualization_params' in self.config: + del self.config['visualization_params'] + with self.logging(self.application.simulator.name): + try: + self.simulation = self.application.simulator.run(self.config) + trials = [] + for i in range(self.config['num_trials']): + trials.append('{}_trial_{}'.format(self.name, i)) + self.write_message({'type': 'trials', + 'data': trials }) + except: + error = 'Something went wrong. Please, try again.' + self.write_message({'type': 'error', + 'error': error}) + self.send_log('ERROR.' + self.application.simulator.name, error) + + def get_trial(self, trial): + G = self.simulation[trial].history_to_graph() + return nx.node_link_data(G) + + @contextmanager + def logging(self, logger): + self.capture_logging = True + self.logger_application = logging.getLogger(logger) + self.log_capture_string = io.StringIO() + ch = logging.StreamHandler(self.log_capture_string) + self.logger_application.addHandler(ch) + self.update_logging() + yield self.capture_logging + + sleep(0.2) + self.log_capture_string.close() + self.logger_application.removeHandler(ch) + self.capture_logging = False + return self.capture_logging + + +class ModularServer(tornado.web.Application): + """ Main visualization application. """ + + port = 8001 + page_handler = (r'/', PageHandler) + socket_handler = (r'/ws', SocketHandler) + static_handler = (r'/(.*)', tornado.web.StaticFileHandler, + {'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, simulator, name='SOIL', verbose=True, *args, **kwargs): + + self.verbose = verbose + self.name = name + self.simulator = simulator + + # Initializing the application itself: + super().__init__(self.handlers, **self.settings) + + 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/soil/web/simulator.py b/soil/web/simulator.py new file mode 100644 index 0000000..5ff9009 --- /dev/null +++ b/soil/web/simulator.py @@ -0,0 +1,36 @@ +import os +import networkx as nx +from soil.simulation import SoilSimulation + + +class Simulator(): + """ Simulator for running simulations. Using SOIL.""" + + def __init__(self, dump=False, dir_path='output'): + self.name = 'soil' + self.dump = dump + self.dir_path = dir_path + + def run(self, config): + name = config['name'] + print('Using config(s): {name}'.format(name=name)) + + sim = SoilSimulation(**config) + sim.dir_path = os.path.join(self.dir_path, name) + sim.dump = self.dump + + print('Dumping results to {} : {}'.format(sim.dir_path, sim.dump)) + + simulation_results = sim.run_simulation() + + # G = simulation_results[0].history_to_graph() + # for node in G.nodes(): + # if 'pos' in G.node[node]: + # G.node[node]['viz'] = {"position": {"x": G.node[node]['pos'][0], "y": G.node[node]['pos'][1], "z": 0.0}} + # del (G.node[node]['pos']) + # nx.write_gexf(G, 'test.gexf', version='1.2draft') + + return simulation_results + + def reset(self): + pass diff --git a/soil/web/templates/css/main.css b/soil/web/templates/css/main.css new file mode 100644 index 0000000..9db8a51 --- /dev/null +++ b/soil/web/templates/css/main.css @@ -0,0 +1,431 @@ + +html, body { + height: 100%; +} + +.carousel { + height: calc(100% - 150px); +} + +.carousel-inner { + height: calc(100% - 50px) !important; +} + +.carousel-inner .item, +.carousel-inner .item .container-fluid { + height: 100%; +} + +.navbar { + box-shadow: 0px 0px 5px 2px rgba(0, 0, 0, .2) +} + +.nav.navbar-right { + margin-right: 10px !important; +} + +.nav.navbar-right a { + outline: none !important; +} + +.dropdown-menu > li > a:hover { + background-color: #d4d3d3; + cursor: pointer; +} + +.wrapper-heading { + display: flex; + flex-direction: row; + padding: 0 !important; +} + +.soil_logo { + padding: 0 !important; + border-left: none !important; + border-right: none !important; + display: flex; + justify-content: flex-end; + background-color: rgb(88, 88, 88); +} + +.soil_logo > img { + max-height: 100%; +} + +.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; + display: flex; + flex-direction: column; + align-items: inherit; + justify-content: space-evenly; +} + +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; +} + +hr { + margin-top: 15px !important; + margin-bottom: 15px !important; + width: 100%; +} + +#update .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; + overflow: hidden; + 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; +} + +#percentTable .no-data-table { + font-size: 10px; + justify-content: center; + align-items: center; + display: flex; + flex: 1; + height: 100%; + font-weight: 100; +} + +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; +} + +.slider { + width: 100% !important; +} + +.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; +} + +/* Console */ + +#update, .console, .soil_logo { + padding: 10px 15px; + height: 135px; + border: 1px solid #585858; +} + +#update { + border-top-right-radius: 5px; + border-bottom-right-radius: 5px; +} + +.container-fluid.fixed { + padding-top: 15px; +} + +.console { + background-color: rgb(88,88,88); + font-family: "Ubuntu Mono"; + font-size: 14px; + font-weight: 500; + color: white; + line-height: 14px; + overflow: auto; + border-top-left-radius: 5px; + border-bottom-left-radius: 5px; + width: 100%; +} + +.console::-webkit-scrollbar { + width: 6px; + background-color: #F5F5F5; +} + +.console::-webkit-scrollbar-thumb { + -webkit-box-shadow: inset 0 0 6px rgba(0,0,0,.3); + background-color: #555; +} + +/** FORMS **/ +.checkbox { + margin-left: 10px !important; +} + +#wrapper-settings { + padding: 15px !important; + height: 100%; + overflow-y: auto; + overflow-x: hidden; +} + +#wrapper-settings.none { + font-weight: bold; + display: flex; + flex: 1; + justify-content: center; + align-items: center; +} + +#wrapper-settings.none:before { + content: 'No configuration provided'; +} + +#wrapper-settings .btn-group button:focus { + background: initial; + border-color: #ccc; +} + +#wrapper-settings .btn-group button { + font-size: xx-small; + padding: 3px 6px; +} + +.item.settings .container-fluid { + padding-top: 10px !important; +} + +#wrapper-settings::-webkit-scrollbar { + width: 6px; + background-color: #F5F5F5; +} + +#wrapper-settings::-webkit-scrollbar-thumb { + -webkit-box-shadow: inset 0 0 6px rgba(0,0,0,.3); + background-color: #ccc; +} + +/** CHARTS **/ +#charts { + height: 100%; + padding-left: 0 !important; + padding-top: 15px !important; + padding-bottom: 15px !important; +} + +.chart { + height: 50%; +} + +.chart.no-data:before { + content: 'No data'; + position: absolute; + font-size: 10px; + padding-bottom: 35px; +} + +.chart.no-data { + font-weight: bold; + display: flex; + flex: 1; + justify-content: center; + align-items: center; +} + +/** MODAL **/ +.modal-footer, +.modal-header { + border: none !important; +} diff --git a/soil/web/templates/css/timeline.css b/soil/web/templates/css/timeline.css new file mode 100644 index 0000000..d6d042d --- /dev/null +++ b/soil/web/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/soil/web/templates/img/background/map.png b/soil/web/templates/img/background/map.png new file mode 100644 index 0000000..84709e0 Binary files /dev/null and b/soil/web/templates/img/background/map.png differ diff --git a/soil/web/templates/img/background/map_4800x2860.jpg b/soil/web/templates/img/background/map_4800x2860.jpg new file mode 100644 index 0000000..b41a0ee Binary files /dev/null and b/soil/web/templates/img/background/map_4800x2860.jpg differ diff --git a/soil/web/templates/img/logo_gsi.svg b/soil/web/templates/img/logo_gsi.svg new file mode 100644 index 0000000..522fb60 --- /dev/null +++ b/soil/web/templates/img/logo_gsi.svg @@ -0,0 +1,140 @@ + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/soil/web/templates/img/logo_soil.png b/soil/web/templates/img/logo_soil.png new file mode 100644 index 0000000..1e6f620 Binary files /dev/null and b/soil/web/templates/img/logo_soil.png differ diff --git a/soil/web/templates/img/svg/home.svg b/soil/web/templates/img/svg/home.svg new file mode 100644 index 0000000..197c39c --- /dev/null +++ b/soil/web/templates/img/svg/home.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/soil/web/templates/img/svg/person.svg b/soil/web/templates/img/svg/person.svg new file mode 100644 index 0000000..fa8f8ad --- /dev/null +++ b/soil/web/templates/img/svg/person.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/soil/web/templates/img/svg/plus.svg b/soil/web/templates/img/svg/plus.svg new file mode 100644 index 0000000..9dd7d5d --- /dev/null +++ b/soil/web/templates/img/svg/plus.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/soil/web/templates/img/svg/target.svg b/soil/web/templates/img/svg/target.svg new file mode 100644 index 0000000..43c0c16 --- /dev/null +++ b/soil/web/templates/img/svg/target.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/soil/web/templates/img/svg/time.svg b/soil/web/templates/img/svg/time.svg new file mode 100644 index 0000000..103dd57 --- /dev/null +++ b/soil/web/templates/img/svg/time.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/soil/web/templates/index.html b/soil/web/templates/index.html new file mode 100644 index 0000000..eb88b6b --- /dev/null +++ b/soil/web/templates/index.html @@ -0,0 +1,378 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {{ name }} + + + + + + + +
+ +
+ +
+ Please, upload a YAML file that defines all the parameters of a simulation.
+ If you don't know how to write the file, please visit this page:
+ http://soilsim.readthedocs.io/en/latest/quickstart.html
+
+ + + + + +
+ +
+ +
+ +
+ + + +
+ Attributes: + +
+ +
+ +
+ + + + + + + + + + + + \ No newline at end of file diff --git a/soil/web/templates/js/socket.js b/soil/web/templates/js/socket.js new file mode 100755 index 0000000..a490116 --- /dev/null +++ b/soil/web/templates/js/socket.js @@ -0,0 +1,460 @@ + +// 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'] + '
'); + $('.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'); + $('