multimodalsim-viewer 0.0.1__py3-none-any.whl → 0.0.3__py3-none-any.whl
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.
- multimodalsim_viewer/common/__init__.py +0 -0
- multimodalsim_viewer/common/environments/.env +4 -0
- multimodalsim_viewer/common/utils.py +223 -0
- multimodalsim_viewer/server/http_routes.py +135 -125
- multimodalsim_viewer/server/log_manager.py +10 -15
- multimodalsim_viewer/server/scripts.py +106 -32
- multimodalsim_viewer/server/server.py +196 -210
- multimodalsim_viewer/server/simulation.py +167 -154
- multimodalsim_viewer/server/simulation_manager.py +570 -607
- multimodalsim_viewer/server/simulation_visualization_data_collector.py +729 -756
- multimodalsim_viewer/server/simulation_visualization_data_model.py +1570 -1693
- multimodalsim_viewer/ui/angular_app.py +40 -0
- multimodalsim_viewer/ui/static/chunk-BQ2VC5TN.js +7 -0
- multimodalsim_viewer/ui/static/{chunk-MTC2LSCT.js → chunk-RHGMGEGM.js} +1 -1
- multimodalsim_viewer/ui/static/environment.json +7 -0
- multimodalsim_viewer/ui/static/images/undefined-texture.png +0 -0
- multimodalsim_viewer/ui/static/images/zoomed-out-stop.png +0 -0
- multimodalsim_viewer/ui/static/index.html +16 -15
- multimodalsim_viewer/ui/static/main-LUPJCMAF.js +3648 -0
- multimodalsim_viewer/ui/static/polyfills-FFHMD2TL.js +2 -2
- multimodalsim_viewer/ui/static/scripts/load-environment.script.js +20 -0
- multimodalsim_viewer/ui/static/styles-KU7LTPET.css +1 -1
- multimodalsim_viewer-0.0.3.dist-info/METADATA +70 -0
- multimodalsim_viewer-0.0.3.dist-info/RECORD +43 -0
- {multimodalsim_viewer-0.0.1.dist-info → multimodalsim_viewer-0.0.3.dist-info}/WHEEL +1 -1
- multimodalsim_viewer-0.0.3.dist-info/entry_points.txt +2 -0
- multimodalsim_viewer/server/server_utils.py +0 -129
- multimodalsim_viewer/ui/cli.py +0 -45
- multimodalsim_viewer/ui/server.py +0 -44
- multimodalsim_viewer/ui/static/chunk-U5CGW4P4.js +0 -7
- multimodalsim_viewer/ui/static/main-X7OVCS3N.js +0 -3648
- multimodalsim_viewer-0.0.1.dist-info/METADATA +0 -21
- multimodalsim_viewer-0.0.1.dist-info/RECORD +0 -38
- multimodalsim_viewer-0.0.1.dist-info/entry_points.txt +0 -8
- /multimodalsim_viewer/ui/static/images/{sample-wait.png → passenger.png} +0 -0
- /multimodalsim_viewer/ui/static/images/{sample-stop.png → stop.png} +0 -0
- /multimodalsim_viewer/ui/static/images/{sample-bus.png → vehicle.png} +0 -0
- /multimodalsim_viewer/ui/static/images/{zoom-out-passenger.png → zoomed-out-passenger.png} +0 -0
- /multimodalsim_viewer/ui/static/images/{zoom-out-vehicle.png → zoomed-out-vehicle.png} +0 -0
- {multimodalsim_viewer-0.0.1.dist-info → multimodalsim_viewer-0.0.3.dist-info}/top_level.txt +0 -0
File without changes
|
@@ -0,0 +1,223 @@
|
|
1
|
+
import datetime
|
2
|
+
import logging
|
3
|
+
import os
|
4
|
+
import shutil
|
5
|
+
import threading
|
6
|
+
from enum import Enum
|
7
|
+
from json import dumps
|
8
|
+
|
9
|
+
from dotenv import dotenv_values
|
10
|
+
from filelock import FileLock
|
11
|
+
from flask import request
|
12
|
+
from flask_socketio import emit
|
13
|
+
|
14
|
+
environment = {}
|
15
|
+
|
16
|
+
|
17
|
+
def load_environment() -> None:
|
18
|
+
# Copy .env if it exists
|
19
|
+
current_directory = os.path.dirname(os.path.abspath(__file__))
|
20
|
+
default_environment_path = os.path.join(current_directory, "../../../.env")
|
21
|
+
environment_path = os.path.join(current_directory, "environments/.env")
|
22
|
+
|
23
|
+
if os.path.exists(default_environment_path):
|
24
|
+
shutil.copy(default_environment_path, environment_path)
|
25
|
+
|
26
|
+
# Load environment variables from .env
|
27
|
+
def load_environment_file(path: str, previous_environment: dict) -> None:
|
28
|
+
if not os.path.exists(path):
|
29
|
+
return
|
30
|
+
|
31
|
+
values = dotenv_values(path)
|
32
|
+
for key in values:
|
33
|
+
previous_environment[key] = values[key]
|
34
|
+
|
35
|
+
# Load default environment
|
36
|
+
load_environment_file(environment_path, environment)
|
37
|
+
# Load environment from the current working directory
|
38
|
+
load_environment_file(os.path.join(os.getcwd(), ".env"), environment)
|
39
|
+
|
40
|
+
# Get host from docker if available and set it in environment
|
41
|
+
environment["HOST"] = os.getenv("HOST", "127.0.0.1")
|
42
|
+
|
43
|
+
# Write environment into static folder
|
44
|
+
static_environment_path = os.path.join(current_directory, "../ui/static/environment.json")
|
45
|
+
lock = FileLock(f"{static_environment_path}.lock")
|
46
|
+
with lock:
|
47
|
+
with open(static_environment_path, "w", encoding="utf-8") as static_environment_file:
|
48
|
+
static_environment_file.write(dumps(environment, indent=2, separators=(",", ": "), sort_keys=True))
|
49
|
+
|
50
|
+
|
51
|
+
class _Environment:
|
52
|
+
is_environment_loaded = False
|
53
|
+
|
54
|
+
def __init__(self):
|
55
|
+
if _Environment.is_environment_loaded:
|
56
|
+
return
|
57
|
+
|
58
|
+
load_environment()
|
59
|
+
_Environment.is_environment_loaded = True
|
60
|
+
print(f"Environment loaded {environment}")
|
61
|
+
|
62
|
+
@property
|
63
|
+
def server_port(self) -> int:
|
64
|
+
return int(environment.get("SERVER_PORT"))
|
65
|
+
|
66
|
+
@property
|
67
|
+
def client_port(self) -> int:
|
68
|
+
return int(environment.get("CLIENT_PORT"))
|
69
|
+
|
70
|
+
@property
|
71
|
+
def host(self) -> str:
|
72
|
+
return environment.get("HOST")
|
73
|
+
|
74
|
+
@property
|
75
|
+
def simulation_save_file_separator(self) -> str:
|
76
|
+
return environment.get("SIMULATION_SAVE_FILE_SEPARATOR")
|
77
|
+
|
78
|
+
@property
|
79
|
+
def input_data_directory_path(self) -> str:
|
80
|
+
return environment.get("INPUT_DATA_DIRECTORY_PATH")
|
81
|
+
|
82
|
+
|
83
|
+
_environment = _Environment()
|
84
|
+
SERVER_PORT = _environment.server_port
|
85
|
+
CLIENT_PORT = _environment.client_port
|
86
|
+
HOST = _environment.host
|
87
|
+
SIMULATION_SAVE_FILE_SEPARATOR = _environment.simulation_save_file_separator
|
88
|
+
INPUT_DATA_DIRECTORY_PATH = _environment.input_data_directory_path
|
89
|
+
|
90
|
+
|
91
|
+
CLIENT_ROOM = "client"
|
92
|
+
SIMULATION_ROOM = "simulation"
|
93
|
+
SCRIPT_ROOM = "script"
|
94
|
+
|
95
|
+
# Save the state of the simulation every STATE_SAVE_STEP events
|
96
|
+
STATE_SAVE_STEP = 1000
|
97
|
+
|
98
|
+
# If the version is identical, the save file can be loaded
|
99
|
+
SAVE_VERSION = 9
|
100
|
+
|
101
|
+
|
102
|
+
class SimulationStatus(Enum):
|
103
|
+
STARTING = "starting"
|
104
|
+
PAUSED = "paused"
|
105
|
+
RUNNING = "running"
|
106
|
+
STOPPING = "stopping"
|
107
|
+
COMPLETED = "completed"
|
108
|
+
LOST = "lost"
|
109
|
+
CORRUPTED = "corrupted"
|
110
|
+
OUTDATED = "outdated"
|
111
|
+
FUTURE = "future"
|
112
|
+
|
113
|
+
|
114
|
+
RUNNING_SIMULATION_STATUSES = [
|
115
|
+
SimulationStatus.STARTING,
|
116
|
+
SimulationStatus.RUNNING,
|
117
|
+
SimulationStatus.PAUSED,
|
118
|
+
SimulationStatus.STOPPING,
|
119
|
+
SimulationStatus.LOST,
|
120
|
+
]
|
121
|
+
|
122
|
+
|
123
|
+
def get_session_id():
|
124
|
+
return request.sid
|
125
|
+
|
126
|
+
|
127
|
+
def build_simulation_id(name: str) -> tuple[str, str]:
|
128
|
+
# Get the current time
|
129
|
+
start_time = datetime.datetime.now().strftime("%Y%m%d-%H%M%S%f")
|
130
|
+
# Remove microseconds
|
131
|
+
start_time = start_time[:-3]
|
132
|
+
|
133
|
+
# Start time first to sort easily
|
134
|
+
simulation_id = f"{start_time}{SIMULATION_SAVE_FILE_SEPARATOR}{name}"
|
135
|
+
return simulation_id, start_time
|
136
|
+
|
137
|
+
|
138
|
+
def get_data_directory_path() -> str:
|
139
|
+
current_file_path = os.path.abspath(__file__)
|
140
|
+
current_file_dir = os.path.dirname(current_file_path)
|
141
|
+
data_directory_path = os.path.join(current_file_dir, "..", "data")
|
142
|
+
|
143
|
+
if not os.path.exists(data_directory_path):
|
144
|
+
os.makedirs(data_directory_path)
|
145
|
+
|
146
|
+
return data_directory_path
|
147
|
+
|
148
|
+
|
149
|
+
def get_saved_logs_directory_path() -> str:
|
150
|
+
data_directory_path = get_data_directory_path()
|
151
|
+
saved_logs_directory_path = os.path.join(data_directory_path, "saved_logs")
|
152
|
+
|
153
|
+
if not os.path.exists(saved_logs_directory_path):
|
154
|
+
os.makedirs(saved_logs_directory_path)
|
155
|
+
|
156
|
+
return saved_logs_directory_path
|
157
|
+
|
158
|
+
|
159
|
+
def get_input_data_directory_path(data: str | None = None) -> str:
|
160
|
+
input_data_directory = INPUT_DATA_DIRECTORY_PATH
|
161
|
+
|
162
|
+
if data is not None:
|
163
|
+
input_data_directory = os.path.join(input_data_directory, data)
|
164
|
+
|
165
|
+
return input_data_directory
|
166
|
+
|
167
|
+
|
168
|
+
def get_available_data():
|
169
|
+
input_data_directory = get_input_data_directory_path()
|
170
|
+
|
171
|
+
if not os.path.exists(input_data_directory):
|
172
|
+
return []
|
173
|
+
|
174
|
+
# List all directories in the input data directory
|
175
|
+
return [
|
176
|
+
name
|
177
|
+
for name in os.listdir(input_data_directory)
|
178
|
+
if os.path.isdir(os.path.join(input_data_directory, name)) and not name.startswith(".")
|
179
|
+
]
|
180
|
+
|
181
|
+
|
182
|
+
def log(message: str, auth_type: str, level=logging.INFO, should_emit=True) -> None:
|
183
|
+
if auth_type == "server":
|
184
|
+
logging.log(level, "[%s] %s", auth_type, message)
|
185
|
+
if should_emit:
|
186
|
+
emit("log", f"{level} [{auth_type}] {message}", to=CLIENT_ROOM)
|
187
|
+
else:
|
188
|
+
logging.log(level, "[%s] %s %s", auth_type, get_session_id(), message)
|
189
|
+
if should_emit:
|
190
|
+
emit(
|
191
|
+
"log",
|
192
|
+
f"{level} [{auth_type}] {get_session_id()} {message}",
|
193
|
+
to=CLIENT_ROOM,
|
194
|
+
)
|
195
|
+
|
196
|
+
|
197
|
+
def verify_simulation_name(name: str | None) -> str | None:
|
198
|
+
if name is None:
|
199
|
+
return "Name is required"
|
200
|
+
if len(name) < 3:
|
201
|
+
return "Name must be at least 3 characters"
|
202
|
+
if len(name) > 50:
|
203
|
+
return "Name must be at most 50 characters"
|
204
|
+
if name.count(SIMULATION_SAVE_FILE_SEPARATOR) > 0:
|
205
|
+
return "Name must not contain three consecutive dashes"
|
206
|
+
if any(char in name for char in ["/", "\\", ":", "*", "?", '"', "<", ">", "|"]):
|
207
|
+
return (
|
208
|
+
'The name muse not contain characters that might affect the file system (e.g. /, \\, :, *, ?, ", <, >, |)'
|
209
|
+
)
|
210
|
+
return None
|
211
|
+
|
212
|
+
|
213
|
+
def set_event_on_input(action: str, key: str, event: threading.Event) -> None:
|
214
|
+
try:
|
215
|
+
user_input = ""
|
216
|
+
while user_input != key:
|
217
|
+
user_input = input(f"Press {key} to {action}: ")
|
218
|
+
|
219
|
+
except EOFError:
|
220
|
+
pass
|
221
|
+
|
222
|
+
print(f"Received {key}: {action}")
|
223
|
+
event.set()
|
@@ -1,125 +1,135 @@
|
|
1
|
-
import
|
2
|
-
import
|
3
|
-
import
|
4
|
-
|
5
|
-
import
|
6
|
-
|
7
|
-
from
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
if file
|
52
|
-
return jsonify({"error": "No
|
53
|
-
|
54
|
-
|
55
|
-
file.
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
"
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
return
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
return
|
1
|
+
import logging
|
2
|
+
import os
|
3
|
+
import shutil
|
4
|
+
import tempfile
|
5
|
+
import zipfile
|
6
|
+
|
7
|
+
from flask import Blueprint, jsonify, request, send_file
|
8
|
+
|
9
|
+
from multimodalsim_viewer.common.utils import get_input_data_directory_path
|
10
|
+
from multimodalsim_viewer.server.simulation_visualization_data_model import (
|
11
|
+
SimulationVisualizationDataManager,
|
12
|
+
)
|
13
|
+
|
14
|
+
http_routes = Blueprint("http_routes", __name__)
|
15
|
+
|
16
|
+
# MARK: Zip Management
|
17
|
+
|
18
|
+
|
19
|
+
def get_unique_folder_name(base_path, folder_name):
|
20
|
+
counter = 1
|
21
|
+
original_name = folder_name
|
22
|
+
while os.path.exists(os.path.join(base_path, folder_name)):
|
23
|
+
folder_name = f"{original_name}_({counter})"
|
24
|
+
counter += 1
|
25
|
+
return folder_name
|
26
|
+
|
27
|
+
|
28
|
+
def zip_folder(folder_path, zip_name):
|
29
|
+
if not os.path.isdir(folder_path):
|
30
|
+
return None
|
31
|
+
|
32
|
+
zip_path = os.path.join(tempfile.gettempdir(), f"{zip_name}.zip")
|
33
|
+
with zipfile.ZipFile(zip_path, "w", zipfile.ZIP_DEFLATED) as zip_file:
|
34
|
+
for root, _, files in os.walk(folder_path):
|
35
|
+
for file in files:
|
36
|
+
file_path = os.path.join(root, file)
|
37
|
+
zip_file.write(file_path, os.path.relpath(file_path, folder_path))
|
38
|
+
|
39
|
+
return zip_path
|
40
|
+
|
41
|
+
|
42
|
+
def handle_zip_upload(folder_path):
|
43
|
+
parent_dir = os.path.dirname(folder_path)
|
44
|
+
base_folder_name = os.path.basename(folder_path)
|
45
|
+
|
46
|
+
unique_folder_name = get_unique_folder_name(parent_dir, base_folder_name)
|
47
|
+
actual_folder_path = os.path.join(parent_dir, unique_folder_name)
|
48
|
+
|
49
|
+
os.makedirs(actual_folder_path, exist_ok=True)
|
50
|
+
|
51
|
+
if "file" not in request.files:
|
52
|
+
return jsonify({"error": "No file part"}), 400
|
53
|
+
|
54
|
+
file = request.files["file"]
|
55
|
+
if file.filename == "":
|
56
|
+
return jsonify({"error": "No selected file"}), 400
|
57
|
+
|
58
|
+
zip_path = os.path.join(tempfile.gettempdir(), file.filename)
|
59
|
+
file.save(zip_path)
|
60
|
+
|
61
|
+
try:
|
62
|
+
with zipfile.ZipFile(zip_path, "r") as zip_ref:
|
63
|
+
zip_ref.extractall(actual_folder_path)
|
64
|
+
logging.info("Extracted files: %s", zip_ref.namelist())
|
65
|
+
|
66
|
+
os.remove(zip_path)
|
67
|
+
except zipfile.BadZipFile:
|
68
|
+
return jsonify({"error": "Invalid ZIP file"}), 400
|
69
|
+
|
70
|
+
response_message = f"Folder '{unique_folder_name}' uploaded successfully"
|
71
|
+
if unique_folder_name != base_folder_name:
|
72
|
+
response_message += f" (renamed from '{base_folder_name}')"
|
73
|
+
|
74
|
+
return (
|
75
|
+
jsonify({"message": response_message, "actual_folder_name": unique_folder_name}),
|
76
|
+
201,
|
77
|
+
)
|
78
|
+
|
79
|
+
|
80
|
+
# MARK: Input Data Routes
|
81
|
+
@http_routes.route("/api/input_data/<folder_name>", methods=["GET"])
|
82
|
+
def export_input_data(folder_name):
|
83
|
+
folder_path = get_input_data_directory_path(folder_name)
|
84
|
+
logging.info("Requested folder: %s", folder_path)
|
85
|
+
|
86
|
+
zip_path = zip_folder(folder_path, folder_name)
|
87
|
+
if not zip_path:
|
88
|
+
return jsonify({"error": "Folder not found"}), 404
|
89
|
+
|
90
|
+
return send_file(zip_path, as_attachment=True)
|
91
|
+
|
92
|
+
|
93
|
+
@http_routes.route("/api/input_data/<folder_name>", methods=["POST"])
|
94
|
+
def import_input_data(folder_name):
|
95
|
+
folder_path = get_input_data_directory_path(folder_name)
|
96
|
+
return handle_zip_upload(folder_path)
|
97
|
+
|
98
|
+
|
99
|
+
@http_routes.route("/api/input_data/<folder_name>", methods=["DELETE"])
|
100
|
+
def delete_input_data(folder_name):
|
101
|
+
folder_path = get_input_data_directory_path(folder_name)
|
102
|
+
if not os.path.isdir(folder_path):
|
103
|
+
return jsonify({"error": "Folder not found"}), 404
|
104
|
+
|
105
|
+
shutil.rmtree(folder_path)
|
106
|
+
return jsonify({"message": f"Folder '{folder_name}' deleted successfully"})
|
107
|
+
|
108
|
+
|
109
|
+
# MARK: Saved Simulations Routes
|
110
|
+
@http_routes.route("/api/simulation/<folder_name>", methods=["GET"])
|
111
|
+
def export_saved_simulation(folder_name):
|
112
|
+
folder_path = SimulationVisualizationDataManager.get_saved_simulation_directory_path(folder_name)
|
113
|
+
logging.info("Requested folder: %s", folder_path)
|
114
|
+
|
115
|
+
zip_path = zip_folder(folder_path, folder_name)
|
116
|
+
if not zip_path:
|
117
|
+
return jsonify({"error": "Folder not found"}), 404
|
118
|
+
|
119
|
+
return send_file(zip_path, as_attachment=True)
|
120
|
+
|
121
|
+
|
122
|
+
@http_routes.route("/api/simulation/<folder_name>", methods=["POST"])
|
123
|
+
def import_saved_simulation(folder_name):
|
124
|
+
folder_path = SimulationVisualizationDataManager.get_saved_simulation_directory_path(folder_name)
|
125
|
+
return handle_zip_upload(folder_path)
|
126
|
+
|
127
|
+
|
128
|
+
@http_routes.route("/api/simulation/<folder_name>", methods=["DELETE"])
|
129
|
+
def delete_saved_simulation(folder_name):
|
130
|
+
folder_path = SimulationVisualizationDataManager.get_saved_simulation_directory_path(folder_name)
|
131
|
+
if not os.path.isdir(folder_path):
|
132
|
+
return jsonify({"error": "Folder not found"}), 404
|
133
|
+
|
134
|
+
shutil.rmtree(folder_path)
|
135
|
+
return jsonify({"message": f"Folder '{folder_name}' deleted successfully"})
|
@@ -1,15 +1,10 @@
|
|
1
|
-
import
|
2
|
-
|
3
|
-
|
4
|
-
def register_log(simulation_id, message):
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
file_path =
|
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")
|
1
|
+
from multimodalsim_viewer.common.utils import get_saved_logs_directory_path
|
2
|
+
|
3
|
+
|
4
|
+
def register_log(simulation_id, message):
|
5
|
+
saved_logs_directory_path = get_saved_logs_directory_path()
|
6
|
+
file_name = f"{simulation_id}.txt"
|
7
|
+
file_path = f"{saved_logs_directory_path}/{file_name}"
|
8
|
+
|
9
|
+
with open(file_path, "a", encoding="utf-8") as file:
|
10
|
+
file.write(message + "\n")
|