multimodalsim-viewer 0.0.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. multimodalsim_viewer-0.0.1/PKG-INFO +21 -0
  2. multimodalsim_viewer-0.0.1/multimodalsim_viewer/__init__.py +0 -0
  3. multimodalsim_viewer-0.0.1/multimodalsim_viewer/server/__init__.py +0 -0
  4. multimodalsim_viewer-0.0.1/multimodalsim_viewer/server/http_routes.py +125 -0
  5. multimodalsim_viewer-0.0.1/multimodalsim_viewer/server/log_manager.py +15 -0
  6. multimodalsim_viewer-0.0.1/multimodalsim_viewer/server/scripts.py +72 -0
  7. multimodalsim_viewer-0.0.1/multimodalsim_viewer/server/server.py +210 -0
  8. multimodalsim_viewer-0.0.1/multimodalsim_viewer/server/server_utils.py +129 -0
  9. multimodalsim_viewer-0.0.1/multimodalsim_viewer/server/simulation.py +154 -0
  10. multimodalsim_viewer-0.0.1/multimodalsim_viewer/server/simulation_manager.py +607 -0
  11. multimodalsim_viewer-0.0.1/multimodalsim_viewer/server/simulation_visualization_data_collector.py +756 -0
  12. multimodalsim_viewer-0.0.1/multimodalsim_viewer/server/simulation_visualization_data_model.py +1693 -0
  13. multimodalsim_viewer-0.0.1/multimodalsim_viewer/ui/__init__.py +0 -0
  14. multimodalsim_viewer-0.0.1/multimodalsim_viewer/ui/cli.py +45 -0
  15. multimodalsim_viewer-0.0.1/multimodalsim_viewer/ui/server.py +44 -0
  16. multimodalsim_viewer-0.0.1/multimodalsim_viewer/ui/static/bitmap-fonts/custom-sans-serif.png +0 -0
  17. multimodalsim_viewer-0.0.1/multimodalsim_viewer/ui/static/bitmap-fonts/custom-sans-serif.xml +1 -0
  18. multimodalsim_viewer-0.0.1/multimodalsim_viewer/ui/static/chunk-MTC2LSCT.js +1 -0
  19. multimodalsim_viewer-0.0.1/multimodalsim_viewer/ui/static/chunk-U5CGW4P4.js +7 -0
  20. multimodalsim_viewer-0.0.1/multimodalsim_viewer/ui/static/favicon.ico +0 -0
  21. multimodalsim_viewer-0.0.1/multimodalsim_viewer/ui/static/images/control-bar.png +0 -0
  22. multimodalsim_viewer-0.0.1/multimodalsim_viewer/ui/static/images/sample-bus.png +0 -0
  23. multimodalsim_viewer-0.0.1/multimodalsim_viewer/ui/static/images/sample-stop.png +0 -0
  24. multimodalsim_viewer-0.0.1/multimodalsim_viewer/ui/static/images/sample-wait.png +0 -0
  25. multimodalsim_viewer-0.0.1/multimodalsim_viewer/ui/static/images/simulation-control-bar.png +0 -0
  26. multimodalsim_viewer-0.0.1/multimodalsim_viewer/ui/static/images/zoom-out-passenger.png +0 -0
  27. multimodalsim_viewer-0.0.1/multimodalsim_viewer/ui/static/images/zoom-out-vehicle.png +0 -0
  28. multimodalsim_viewer-0.0.1/multimodalsim_viewer/ui/static/index.html +15 -0
  29. multimodalsim_viewer-0.0.1/multimodalsim_viewer/ui/static/main-X7OVCS3N.js +3648 -0
  30. multimodalsim_viewer-0.0.1/multimodalsim_viewer/ui/static/media/layers-2x-TBM42ERR.png +0 -0
  31. multimodalsim_viewer-0.0.1/multimodalsim_viewer/ui/static/media/layers-55W3Q4RM.png +0 -0
  32. multimodalsim_viewer-0.0.1/multimodalsim_viewer/ui/static/media/marker-icon-2V3QKKVC.png +0 -0
  33. multimodalsim_viewer-0.0.1/multimodalsim_viewer/ui/static/polyfills-FFHMD2TL.js +2 -0
  34. multimodalsim_viewer-0.0.1/multimodalsim_viewer/ui/static/styles-KU7LTPET.css +1 -0
  35. multimodalsim_viewer-0.0.1/multimodalsim_viewer.egg-info/PKG-INFO +21 -0
  36. multimodalsim_viewer-0.0.1/multimodalsim_viewer.egg-info/SOURCES.txt +40 -0
  37. multimodalsim_viewer-0.0.1/multimodalsim_viewer.egg-info/dependency_links.txt +1 -0
  38. multimodalsim_viewer-0.0.1/multimodalsim_viewer.egg-info/entry_points.txt +8 -0
  39. multimodalsim_viewer-0.0.1/multimodalsim_viewer.egg-info/requires.txt +9 -0
  40. multimodalsim_viewer-0.0.1/multimodalsim_viewer.egg-info/top_level.txt +1 -0
  41. multimodalsim_viewer-0.0.1/setup.cfg +4 -0
  42. multimodalsim_viewer-0.0.1/setup.py +44 -0
@@ -0,0 +1,21 @@
1
+ Metadata-Version: 2.4
2
+ Name: multimodalsim_viewer
3
+ Version: 0.0.1
4
+ Summary: Multimodal simulation viewer
5
+ License: MIT
6
+ Keywords: flask angular ui multimodal server
7
+ Requires-Python: ==3.11.*
8
+ Requires-Dist: flask==3.1.1
9
+ Requires-Dist: flask-socketio==5.5.1
10
+ Requires-Dist: eventlet==0.40.0
11
+ Requires-Dist: websocket-client==1.8.0
12
+ Requires-Dist: filelock==3.18.0
13
+ Requires-Dist: flask_cors==6.0.0
14
+ Requires-Dist: questionary==2.1.0
15
+ Requires-Dist: python-dotenv==1.1.0
16
+ Requires-Dist: multimodalsim==0.0.1
17
+ Dynamic: keywords
18
+ Dynamic: license
19
+ Dynamic: requires-dist
20
+ Dynamic: requires-python
21
+ Dynamic: summary
@@ -0,0 +1,125 @@
1
+ import os
2
+ import shutil
3
+ import zipfile
4
+ from flask import Blueprint, request, jsonify, send_file
5
+ import logging
6
+ import tempfile
7
+ from pathlib import Path
8
+
9
+ BASE_DIR = Path(__file__).resolve().parent.parent
10
+ data_dir = BASE_DIR / "data"
11
+ saved_simulations_dir = Path(__file__).parent / "saved_simulations"
12
+
13
+ http_routes = Blueprint("http_routes", __name__)
14
+
15
+ # MARK: Zip Management
16
+
17
+ def get_unique_folder_name(base_path, folder_name):
18
+ counter = 1
19
+ original_name = folder_name
20
+ while os.path.exists(os.path.join(base_path, folder_name)):
21
+ folder_name = f"{original_name}_({counter})"
22
+ counter += 1
23
+ return folder_name
24
+
25
+ def zip_folder(folder_path, zip_name):
26
+ if not os.path.isdir(folder_path):
27
+ return None
28
+
29
+ zip_path = os.path.join(tempfile.gettempdir(), f"{zip_name}.zip")
30
+ with zipfile.ZipFile(zip_path, 'w', zipfile.ZIP_DEFLATED) as zipf:
31
+ for root, _, files in os.walk(folder_path):
32
+ for file in files:
33
+ file_path = os.path.join(root, file)
34
+ zipf.write(file_path, os.path.relpath(file_path, folder_path))
35
+
36
+ return zip_path
37
+
38
+ def handle_zip_upload(folder_path):
39
+ parent_dir = os.path.dirname(folder_path)
40
+ base_folder_name = os.path.basename(folder_path)
41
+
42
+ unique_folder_name = get_unique_folder_name(parent_dir, base_folder_name)
43
+ actual_folder_path = os.path.join(parent_dir, unique_folder_name)
44
+
45
+ os.makedirs(actual_folder_path, exist_ok=True)
46
+
47
+ if 'file' not in request.files:
48
+ return jsonify({"error": "No file part"}), 400
49
+
50
+ file = request.files['file']
51
+ if file.filename == '':
52
+ return jsonify({"error": "No selected file"}), 400
53
+
54
+ zip_path = os.path.join(tempfile.gettempdir(), file.filename)
55
+ file.save(zip_path)
56
+
57
+ try:
58
+ with zipfile.ZipFile(zip_path, 'r') as zip_ref:
59
+ zip_ref.extractall(actual_folder_path)
60
+ logging.info(f"Extracted files: {zip_ref.namelist()}")
61
+
62
+ os.remove(zip_path)
63
+ except zipfile.BadZipFile:
64
+ return jsonify({"error": "Invalid ZIP file"}), 400
65
+
66
+ response_message = f"Folder '{unique_folder_name}' uploaded successfully"
67
+ if unique_folder_name != base_folder_name:
68
+ response_message += f" (renamed from '{base_folder_name}')"
69
+
70
+ return jsonify({
71
+ "message": response_message,
72
+ "actual_folder_name": unique_folder_name
73
+ }), 201
74
+
75
+ # MARK: Input Data Routes
76
+ @http_routes.route("/api/input_data/<folder_name>", methods=["GET"])
77
+ def export_input_data(folder_name):
78
+ folder_path = os.path.join(data_dir, folder_name)
79
+ logging.info(f"Requested folder: {folder_path}")
80
+
81
+ zip_path = zip_folder(folder_path, folder_name)
82
+ if not zip_path:
83
+ return jsonify({"error": "Folder not found"}), 404
84
+
85
+ return send_file(zip_path, as_attachment=True)
86
+
87
+ @http_routes.route("/api/input_data/<folder_name>", methods=["POST"])
88
+ def import_input_data(folder_name):
89
+ folder_path = os.path.join(data_dir, folder_name)
90
+ return handle_zip_upload(folder_path)
91
+
92
+ @http_routes.route("/api/input_data/<folder_name>", methods=["DELETE"])
93
+ def delete_input_data(folder_name):
94
+ folder_path = os.path.join(data_dir, folder_name)
95
+ if not os.path.isdir(folder_path):
96
+ return jsonify({"error": "Folder not found"}), 404
97
+
98
+ shutil.rmtree(folder_path)
99
+ return jsonify({"message": f"Folder '{folder_name}' deleted successfully"})
100
+
101
+ # MARK: Saved Simulations Routes
102
+ @http_routes.route("/api/simulation/<folder_name>", methods=["GET"])
103
+ def export_saved_simulation(folder_name):
104
+ folder_path = os.path.join(saved_simulations_dir, folder_name)
105
+ logging.info(f"Requested folder: {folder_path}")
106
+
107
+ zip_path = zip_folder(folder_path, folder_name)
108
+ if not zip_path:
109
+ return jsonify({"error": "Folder not found"}), 404
110
+
111
+ return send_file(zip_path, as_attachment=True)
112
+
113
+ @http_routes.route("/api/simulation/<folder_name>", methods=["POST"])
114
+ def import_saved_simulation(folder_name):
115
+ folder_path = os.path.join(saved_simulations_dir, folder_name)
116
+ return handle_zip_upload(folder_path)
117
+
118
+ @http_routes.route("/api/simulation/<folder_name>", methods=["DELETE"])
119
+ def delete_saved_simulation(folder_name):
120
+ folder_path = os.path.join(saved_simulations_dir, folder_name)
121
+ if not os.path.isdir(folder_path):
122
+ return jsonify({"error": "Folder not found"}), 404
123
+
124
+ shutil.rmtree(folder_path)
125
+ return jsonify({"message": f"Folder '{folder_name}' deleted successfully"})
@@ -0,0 +1,15 @@
1
+ import os
2
+
3
+
4
+ def register_log(simulation_id, message):
5
+ current_directory = os.path.dirname(os.path.abspath(__file__))
6
+ log_directory_name = "saved_logs"
7
+ log_directory_path = f"{current_directory}/{log_directory_name}"
8
+ file_name = f"{simulation_id}.txt"
9
+ file_path = f"{log_directory_path}/{file_name}"
10
+
11
+ if not os.path.exists(log_directory_path):
12
+ os.makedirs(log_directory_path)
13
+
14
+ with open(file_path, "a") as file:
15
+ file.write(message + "\n")
@@ -0,0 +1,72 @@
1
+ import threading
2
+ import time
3
+
4
+ import requests
5
+ from multimodalsim_viewer.server.server import run_server
6
+ from multimodalsim_viewer.server.server_utils import CLIENT_PORT, HOST, PORT
7
+ from multimodalsim_viewer.ui.cli import main as run_ui
8
+ from socketio import Client, exceptions
9
+
10
+
11
+ def run_server_and_ui():
12
+ # Start the server in a separate thread
13
+ server_thread = threading.Thread(target=run_server)
14
+ server_thread.start()
15
+
16
+ # Start the UI in a separate thread
17
+ ui_thread = threading.Thread(target=run_ui)
18
+ ui_thread.start()
19
+
20
+ # Wait for both threads to finish
21
+ server_thread.join()
22
+ ui_thread.join()
23
+
24
+
25
+ def terminate_server():
26
+ print("Terminating server...")
27
+
28
+ sio = Client()
29
+
30
+ try:
31
+ sio.connect(f"http://{HOST}:{PORT}", auth={"type": "script"})
32
+
33
+ sio.emit("terminate")
34
+
35
+ time.sleep(1)
36
+
37
+ sio.disconnect()
38
+
39
+ print("Server terminated")
40
+
41
+ except exceptions.ConnectionError as e:
42
+ print(f"Failed to connect to server (server not running?): {e}")
43
+
44
+ except Exception as e:
45
+ print(f"Error: {e}")
46
+
47
+
48
+ def terminate_ui():
49
+ print("Terminating UI...")
50
+
51
+ try:
52
+ response = requests.get(f"http://{HOST}:{CLIENT_PORT}/terminate")
53
+
54
+ if response.status_code == 200:
55
+ print("UI terminated")
56
+ else:
57
+ print(f"Failed to terminate UI: {response.status_code}")
58
+
59
+ except requests.exceptions.RequestException as e:
60
+ print(f"Error: {e}")
61
+
62
+ except Exception as e:
63
+ print(f"Error: {e}")
64
+
65
+
66
+ def terminate_all():
67
+ print("Terminating all...")
68
+
69
+ terminate_server()
70
+ terminate_ui()
71
+
72
+ print("All terminated")
@@ -0,0 +1,210 @@
1
+ import logging
2
+ import time
3
+
4
+ from flask import Flask
5
+ from flask_cors import CORS
6
+ from flask_socketio import SocketIO, emit, join_room, leave_room
7
+ from multimodalsim_viewer.server.http_routes import http_routes
8
+ from multimodalsim_viewer.server.server_utils import (
9
+ CLIENT_ROOM,
10
+ HOST,
11
+ PORT,
12
+ get_available_data,
13
+ get_session_id,
14
+ log,
15
+ )
16
+ from multimodalsim_viewer.server.simulation_manager import SimulationManager
17
+
18
+
19
+ def run_server():
20
+ app = Flask(__name__)
21
+
22
+ # Register HTTP routes
23
+ CORS(app)
24
+ app.register_blueprint(http_routes)
25
+
26
+ socketio = SocketIO(app, cors_allowed_origins="*")
27
+
28
+ # key = session id, value = auth type
29
+ sockets_types_by_session_id = dict()
30
+
31
+ simulation_manager = SimulationManager()
32
+
33
+ # MARK: Main events
34
+ @socketio.on("connect")
35
+ def on_connect(auth):
36
+ auth_type = auth["type"]
37
+ log("connected", auth_type)
38
+ sockets_types_by_session_id[get_session_id()] = auth_type
39
+ join_room(auth_type)
40
+
41
+ @socketio.on("disconnect")
42
+ def on_disconnect(reason):
43
+ session_id = get_session_id()
44
+ auth_type = sockets_types_by_session_id.pop(session_id)
45
+ log(f"disconnected: {reason}", auth_type)
46
+ leave_room(auth_type)
47
+
48
+ if auth_type == "simulation":
49
+ simulation_manager.on_simulation_disconnect(session_id)
50
+
51
+ # MARK: Client events
52
+ @socketio.on("start-simulation")
53
+ def on_client_start_simulation(name, data, response_event, max_duration):
54
+ log(
55
+ f"starting simulation {name} with data {data}, response event {response_event} and max duration {max_duration}",
56
+ "client",
57
+ )
58
+ simulation_manager.start_simulation(name, data, response_event, max_duration)
59
+
60
+ @socketio.on("stop-simulation")
61
+ def on_client_stop_simulation(simulation_id):
62
+ log(f"stopping simulation {simulation_id}", "client")
63
+ simulation_manager.stop_simulation(simulation_id)
64
+
65
+ @socketio.on("pause-simulation")
66
+ def on_client_pause_simulation(simulation_id):
67
+ log(f"pausing simulation {simulation_id}", "client")
68
+ simulation_manager.pause_simulation(simulation_id)
69
+
70
+ @socketio.on("resume-simulation")
71
+ def on_client_resume_simulation(simulation_id):
72
+ log(f"resuming simulation {simulation_id}", "client")
73
+ simulation_manager.resume_simulation(simulation_id)
74
+
75
+ @socketio.on("get-simulations")
76
+ def on_client_get_simulations():
77
+ log("getting simulations", "client")
78
+ simulation_manager.emit_simulations()
79
+
80
+ @socketio.on("get-available-data")
81
+ def on_client_get_data():
82
+ log("getting available data", "client")
83
+ emit("available-data", get_available_data(), to=CLIENT_ROOM)
84
+
85
+ @socketio.on("get-missing-simulation-states")
86
+ def on_client_get_missing_simulation_states(
87
+ simulation_id, visualization_time, loaded_state_orders
88
+ ):
89
+ log(
90
+ f"getting missing simulation states for {simulation_id} with visualization time {visualization_time} and {len(loaded_state_orders)} loaded state orders ",
91
+ "client",
92
+ )
93
+ simulation_manager.emit_missing_simulation_states(
94
+ simulation_id, visualization_time, loaded_state_orders
95
+ )
96
+
97
+ @socketio.on("get-polylines")
98
+ def on_client_get_polylines(simulation_id):
99
+ log(f"getting polylines for {simulation_id}", "client")
100
+ simulation_manager.emit_simulation_polylines(simulation_id)
101
+
102
+ @socketio.on("edit-simulation-configuration")
103
+ def on_client_edit_simulation_configuration(simulation_id, max_duration):
104
+ log(
105
+ f"editing simulation {simulation_id} configuration with max duration {max_duration}",
106
+ "client",
107
+ )
108
+ simulation_manager.edit_simulation_configuration(simulation_id, max_duration)
109
+
110
+ # MARK: Script events
111
+ @socketio.on("terminate")
112
+ def on_script_terminate():
113
+ log("terminating server", "script")
114
+
115
+ for simulation_id, simulation_handler in simulation_manager.simulations.items():
116
+ if simulation_handler.process is not None:
117
+ simulation_manager.stop_simulation(simulation_id)
118
+ simulation_handler.process.join()
119
+
120
+ # TODO Solution to remove sleep
121
+ # - Add a flag to the simulation manager to stop the server
122
+ # - On simulation-end, check if all simulations with processes are stopped
123
+ # - If so, stop the server
124
+ time.sleep(1)
125
+
126
+ socketio.stop()
127
+
128
+ # MARK: Simulation events
129
+ @socketio.on("simulation-start")
130
+ def on_simulation_start(simulation_id, simulation_start_time):
131
+ log(f"simulation {simulation_id} started", "simulation")
132
+ simulation_manager.on_simulation_start(
133
+ simulation_id, get_session_id(), simulation_start_time
134
+ )
135
+
136
+ @socketio.on("simulation-pause")
137
+ def on_simulation_pause(simulation_id):
138
+ log(f"simulation {simulation_id} paused", "simulation")
139
+ simulation_manager.on_simulation_pause(simulation_id)
140
+
141
+ @socketio.on("simulation-resume")
142
+ def on_simulation_resume(simulation_id):
143
+ log(f"simulation {simulation_id} resumed", "simulation")
144
+ simulation_manager.on_simulation_resume(simulation_id)
145
+
146
+ @socketio.on("log")
147
+ def on_simulation_log(simulation_id, message):
148
+ log(f"simulation {simulation_id}: {message}", "simulation", logging.DEBUG)
149
+
150
+ @socketio.on("simulation-update-time")
151
+ def on_simulation_update_time(simulation_id, timestamp):
152
+ log(
153
+ f"simulation {simulation_id} time: {timestamp}",
154
+ "simulation",
155
+ logging.DEBUG,
156
+ )
157
+ simulation_manager.on_simulation_update_time(simulation_id, timestamp)
158
+
159
+ @socketio.on("simulation-update-estimated-end-time")
160
+ def on_simulation_update_estimated_end_time(simulation_id, estimated_end_time):
161
+ log(
162
+ f"simulation {simulation_id} estimated end time: {estimated_end_time}",
163
+ "simulation",
164
+ logging.DEBUG,
165
+ )
166
+ simulation_manager.on_simulation_update_estimated_end_time(
167
+ simulation_id, estimated_end_time
168
+ )
169
+
170
+ @socketio.on("simulation-update-polylines-version")
171
+ def on_simulation_update_polylines_version(simulation_id):
172
+ log(f"simulation {simulation_id} polylines version updated", "simulation")
173
+
174
+ simulation_manager.on_simulation_update_polylines_version(simulation_id)
175
+
176
+ @socketio.on("simulation-identification")
177
+ def on_simulation_identification(
178
+ simulation_id,
179
+ data,
180
+ simulation_start_time,
181
+ timestamp,
182
+ estimated_end_time,
183
+ max_duration,
184
+ status,
185
+ ):
186
+ log(
187
+ f"simulation {simulation_id} identified with data {data}, simulation start time {simulation_start_time}, timestamp {timestamp}, estimated end time {estimated_end_time}, max duration {max_duration} and status {status}",
188
+ "simulation",
189
+ )
190
+ simulation_manager.on_simulation_identification(
191
+ simulation_id,
192
+ data,
193
+ simulation_start_time,
194
+ timestamp,
195
+ estimated_end_time,
196
+ max_duration,
197
+ status,
198
+ get_session_id(),
199
+ )
200
+
201
+ logging.basicConfig(level=logging.DEBUG)
202
+
203
+ log(f"Starting server at {HOST}:{PORT}", "server", should_emit=False)
204
+
205
+ # MARK: Run server
206
+ socketio.run(app, host=HOST, port=PORT)
207
+
208
+
209
+ if __name__ == "__main__":
210
+ run_server()
@@ -0,0 +1,129 @@
1
+ import datetime
2
+ import logging
3
+ import os
4
+ import threading
5
+ from enum import Enum
6
+ from pathlib import Path
7
+
8
+ from dotenv import load_dotenv
9
+ from flask import request
10
+ from flask_socketio import emit
11
+
12
+ # Load environment variables from the root .env file
13
+ env_path = Path(__file__).parent.parent / ".env"
14
+ load_dotenv(env_path)
15
+
16
+ HOST = os.getenv("SERVER_HOST", "127.0.0.1")
17
+ PORT = int(os.getenv("PORT_SERVER", "8089")) # It will use .env or default to 8089
18
+ CLIENT_PORT = int(
19
+ os.getenv("PORT_CLIENT", "8085")
20
+ ) # It will use .env or default to 8085
21
+
22
+ CLIENT_ROOM = "client"
23
+ SIMULATION_ROOM = "simulation"
24
+ SCRIPT_ROOM = "script"
25
+
26
+ # Save the state of the simulation every STATE_SAVE_STEP events
27
+ STATE_SAVE_STEP = 1000
28
+
29
+ # If the version is identical, the save file can be loaded
30
+ SAVE_VERSION = 9
31
+
32
+ SIMULATION_SAVE_FILE_SEPARATOR = "---"
33
+
34
+
35
+ class SimulationStatus(Enum):
36
+ STARTING = "starting"
37
+ PAUSED = "paused"
38
+ RUNNING = "running"
39
+ STOPPING = "stopping"
40
+ COMPLETED = "completed"
41
+ LOST = "lost"
42
+ CORRUPTED = "corrupted"
43
+ OUTDATED = "outdated"
44
+ FUTURE = "future"
45
+
46
+
47
+ RUNNING_SIMULATION_STATUSES = [
48
+ SimulationStatus.STARTING,
49
+ SimulationStatus.RUNNING,
50
+ SimulationStatus.PAUSED,
51
+ SimulationStatus.STOPPING,
52
+ SimulationStatus.LOST,
53
+ ]
54
+
55
+
56
+ def get_session_id():
57
+ return request.sid
58
+
59
+
60
+ def build_simulation_id(name: str) -> tuple[str, str]:
61
+ # Get the current time
62
+ start_time = datetime.datetime.now().strftime("%Y%m%d-%H%M%S%f")
63
+ # Remove microseconds
64
+ start_time = start_time[:-3]
65
+
66
+ # Start time first to sort easily
67
+ simulation_id = f"{start_time}{SIMULATION_SAVE_FILE_SEPARATOR}{name}"
68
+ return simulation_id, start_time
69
+
70
+
71
+ def get_data_directory_path(data: str | None = None) -> str:
72
+ cwd = os.getcwd()
73
+ data_directory = os.path.join(cwd, "data")
74
+
75
+ if data is not None:
76
+ data_directory = os.path.join(data_directory, data)
77
+
78
+ return data_directory
79
+
80
+
81
+ def get_available_data():
82
+ data_dir = get_data_directory_path()
83
+
84
+ if not os.path.exists(data_dir):
85
+ return []
86
+
87
+ return os.listdir(data_dir)
88
+
89
+
90
+ def log(message: str, auth_type: str, level=logging.INFO, should_emit=True) -> None:
91
+ if auth_type == "server":
92
+ logging.log(level, f"[{auth_type}] {message}")
93
+ if should_emit:
94
+ emit("log", f"{level} [{auth_type}] {message}", to=CLIENT_ROOM)
95
+ else:
96
+ logging.log(level, f"[{auth_type}] {get_session_id()} {message}")
97
+ if should_emit:
98
+ emit(
99
+ "log",
100
+ f"{level} [{auth_type}] {get_session_id()} {message}",
101
+ to=CLIENT_ROOM,
102
+ )
103
+
104
+
105
+ def verify_simulation_name(name: str | None) -> str | None:
106
+ if name is None:
107
+ return "Name is required"
108
+ elif len(name) < 3:
109
+ return "Name must be at least 3 characters"
110
+ elif len(name) > 50:
111
+ return "Name must be at most 50 characters"
112
+ elif name.count(SIMULATION_SAVE_FILE_SEPARATOR) > 0:
113
+ return "Name must not contain three consecutive dashes"
114
+ elif any(char in name for char in ["/", "\\", ":", "*", "?", '"', "<", ">", "|"]):
115
+ return 'The name muse not contain characters that might affect the file system (e.g. /, \, :, *, ?, ", <, >, |)'
116
+ return None
117
+
118
+
119
+ def set_event_on_input(action: str, key: str, event: threading.Event) -> None:
120
+ try:
121
+ user_input = ""
122
+ while user_input != key:
123
+ user_input = input(f"Press {key} to {action}: ")
124
+
125
+ except EOFError:
126
+ pass
127
+
128
+ print(f"Received {key}: {action}")
129
+ event.set()