multimodalsim-viewer 0.0.1__tar.gz → 0.0.2__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.
- multimodalsim_viewer-0.0.2/MANIFEST.in +7 -0
- multimodalsim_viewer-0.0.2/PKG-INFO +70 -0
- multimodalsim_viewer-0.0.2/README.md +37 -0
- multimodalsim_viewer-0.0.2/multimodalsim_viewer/common/environments/.env +4 -0
- multimodalsim_viewer-0.0.2/multimodalsim_viewer/common/utils.py +223 -0
- {multimodalsim_viewer-0.0.1 → multimodalsim_viewer-0.0.2}/multimodalsim_viewer/server/http_routes.py +135 -125
- multimodalsim_viewer-0.0.2/multimodalsim_viewer/server/log_manager.py +10 -0
- multimodalsim_viewer-0.0.2/multimodalsim_viewer/server/scripts.py +146 -0
- {multimodalsim_viewer-0.0.1 → multimodalsim_viewer-0.0.2}/multimodalsim_viewer/server/server.py +196 -210
- {multimodalsim_viewer-0.0.1 → multimodalsim_viewer-0.0.2}/multimodalsim_viewer/server/simulation.py +167 -154
- {multimodalsim_viewer-0.0.1 → multimodalsim_viewer-0.0.2}/multimodalsim_viewer/server/simulation_manager.py +570 -607
- {multimodalsim_viewer-0.0.1 → multimodalsim_viewer-0.0.2}/multimodalsim_viewer/server/simulation_visualization_data_collector.py +729 -756
- {multimodalsim_viewer-0.0.1 → multimodalsim_viewer-0.0.2}/multimodalsim_viewer/server/simulation_visualization_data_model.py +1552 -1693
- multimodalsim_viewer-0.0.2/multimodalsim_viewer/ui/__init__.py +0 -0
- multimodalsim_viewer-0.0.2/multimodalsim_viewer/ui/angular_app.py +40 -0
- multimodalsim_viewer-0.0.2/multimodalsim_viewer/ui/static/chunk-6VAXIXEZ.js +7 -0
- multimodalsim_viewer-0.0.1/multimodalsim_viewer/ui/static/chunk-MTC2LSCT.js → multimodalsim_viewer-0.0.2/multimodalsim_viewer/ui/static/chunk-IGIP6IOU.js +1 -1
- multimodalsim_viewer-0.0.2/multimodalsim_viewer/ui/static/environment.json +7 -0
- {multimodalsim_viewer-0.0.1 → multimodalsim_viewer-0.0.2}/multimodalsim_viewer/ui/static/index.html +16 -15
- multimodalsim_viewer-0.0.1/multimodalsim_viewer/ui/static/main-X7OVCS3N.js → multimodalsim_viewer-0.0.2/multimodalsim_viewer/ui/static/main-FGMGJ32M.js +3648 -3648
- {multimodalsim_viewer-0.0.1 → multimodalsim_viewer-0.0.2}/multimodalsim_viewer/ui/static/polyfills-FFHMD2TL.js +2 -2
- multimodalsim_viewer-0.0.2/multimodalsim_viewer/ui/static/scripts/load-environment.script.js +20 -0
- {multimodalsim_viewer-0.0.1 → multimodalsim_viewer-0.0.2}/multimodalsim_viewer/ui/static/styles-KU7LTPET.css +1 -1
- multimodalsim_viewer-0.0.2/multimodalsim_viewer.egg-info/PKG-INFO +70 -0
- {multimodalsim_viewer-0.0.1 → multimodalsim_viewer-0.0.2}/multimodalsim_viewer.egg-info/SOURCES.txt +13 -7
- multimodalsim_viewer-0.0.2/multimodalsim_viewer.egg-info/entry_points.txt +2 -0
- {multimodalsim_viewer-0.0.1 → multimodalsim_viewer-0.0.2}/multimodalsim_viewer.egg-info/requires.txt +11 -0
- multimodalsim_viewer-0.0.2/pyproject.toml +5 -0
- {multimodalsim_viewer-0.0.1 → multimodalsim_viewer-0.0.2}/setup.cfg +4 -4
- multimodalsim_viewer-0.0.2/setup.py +57 -0
- multimodalsim_viewer-0.0.1/PKG-INFO +0 -21
- multimodalsim_viewer-0.0.1/multimodalsim_viewer/server/log_manager.py +0 -15
- multimodalsim_viewer-0.0.1/multimodalsim_viewer/server/scripts.py +0 -72
- multimodalsim_viewer-0.0.1/multimodalsim_viewer/server/server_utils.py +0 -129
- multimodalsim_viewer-0.0.1/multimodalsim_viewer/ui/cli.py +0 -45
- multimodalsim_viewer-0.0.1/multimodalsim_viewer/ui/server.py +0 -44
- multimodalsim_viewer-0.0.1/multimodalsim_viewer/ui/static/chunk-U5CGW4P4.js +0 -7
- multimodalsim_viewer-0.0.1/multimodalsim_viewer.egg-info/PKG-INFO +0 -21
- multimodalsim_viewer-0.0.1/multimodalsim_viewer.egg-info/entry_points.txt +0 -8
- multimodalsim_viewer-0.0.1/setup.py +0 -44
- {multimodalsim_viewer-0.0.1 → multimodalsim_viewer-0.0.2}/multimodalsim_viewer/__init__.py +0 -0
- {multimodalsim_viewer-0.0.1/multimodalsim_viewer/server → multimodalsim_viewer-0.0.2/multimodalsim_viewer/common}/__init__.py +0 -0
- {multimodalsim_viewer-0.0.1/multimodalsim_viewer/ui → multimodalsim_viewer-0.0.2/multimodalsim_viewer/server}/__init__.py +0 -0
- {multimodalsim_viewer-0.0.1 → multimodalsim_viewer-0.0.2}/multimodalsim_viewer/ui/static/bitmap-fonts/custom-sans-serif.png +0 -0
- {multimodalsim_viewer-0.0.1 → multimodalsim_viewer-0.0.2}/multimodalsim_viewer/ui/static/bitmap-fonts/custom-sans-serif.xml +0 -0
- {multimodalsim_viewer-0.0.1 → multimodalsim_viewer-0.0.2}/multimodalsim_viewer/ui/static/favicon.ico +0 -0
- {multimodalsim_viewer-0.0.1 → multimodalsim_viewer-0.0.2}/multimodalsim_viewer/ui/static/images/control-bar.png +0 -0
- {multimodalsim_viewer-0.0.1 → multimodalsim_viewer-0.0.2}/multimodalsim_viewer/ui/static/images/sample-bus.png +0 -0
- {multimodalsim_viewer-0.0.1 → multimodalsim_viewer-0.0.2}/multimodalsim_viewer/ui/static/images/sample-stop.png +0 -0
- {multimodalsim_viewer-0.0.1 → multimodalsim_viewer-0.0.2}/multimodalsim_viewer/ui/static/images/sample-wait.png +0 -0
- {multimodalsim_viewer-0.0.1 → multimodalsim_viewer-0.0.2}/multimodalsim_viewer/ui/static/images/simulation-control-bar.png +0 -0
- {multimodalsim_viewer-0.0.1 → multimodalsim_viewer-0.0.2}/multimodalsim_viewer/ui/static/images/zoom-out-passenger.png +0 -0
- {multimodalsim_viewer-0.0.1 → multimodalsim_viewer-0.0.2}/multimodalsim_viewer/ui/static/images/zoom-out-vehicle.png +0 -0
- {multimodalsim_viewer-0.0.1 → multimodalsim_viewer-0.0.2}/multimodalsim_viewer/ui/static/media/layers-2x-TBM42ERR.png +0 -0
- {multimodalsim_viewer-0.0.1 → multimodalsim_viewer-0.0.2}/multimodalsim_viewer/ui/static/media/layers-55W3Q4RM.png +0 -0
- {multimodalsim_viewer-0.0.1 → multimodalsim_viewer-0.0.2}/multimodalsim_viewer/ui/static/media/marker-icon-2V3QKKVC.png +0 -0
- {multimodalsim_viewer-0.0.1 → multimodalsim_viewer-0.0.2}/multimodalsim_viewer.egg-info/dependency_links.txt +0 -0
- {multimodalsim_viewer-0.0.1 → multimodalsim_viewer-0.0.2}/multimodalsim_viewer.egg-info/top_level.txt +0 -0
@@ -0,0 +1,70 @@
|
|
1
|
+
Metadata-Version: 2.4
|
2
|
+
Name: multimodalsim_viewer
|
3
|
+
Version: 0.0.2
|
4
|
+
Summary: Multimodal simulation viewer
|
5
|
+
Keywords: flask angular ui multimodal server
|
6
|
+
Requires-Python: >=3.10
|
7
|
+
Description-Content-Type: text/markdown
|
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
|
+
Requires-Dist: get_latest_version==1.0.3
|
18
|
+
Requires-Dist: setuptools==80.9.0
|
19
|
+
Provides-Extra: dev
|
20
|
+
Requires-Dist: black==25.1.0; extra == "dev"
|
21
|
+
Requires-Dist: pylint==3.3.7; extra == "dev"
|
22
|
+
Requires-Dist: isort==6.0.1; extra == "dev"
|
23
|
+
Provides-Extra: build
|
24
|
+
Requires-Dist: build; extra == "build"
|
25
|
+
Requires-Dist: twine; extra == "build"
|
26
|
+
Dynamic: description
|
27
|
+
Dynamic: description-content-type
|
28
|
+
Dynamic: keywords
|
29
|
+
Dynamic: provides-extra
|
30
|
+
Dynamic: requires-dist
|
31
|
+
Dynamic: requires-python
|
32
|
+
Dynamic: summary
|
33
|
+
|
34
|
+
# multimodalsim-viewer
|
35
|
+
|
36
|
+
This package provides an interface to the [multimodalsim simulation project](https://pypi.org/project/multimodalsim/), allowing you to run and visualize simulations easily through a web interface.
|
37
|
+
|
38
|
+
## Usage
|
39
|
+
|
40
|
+
You have access to several commands that will allow you to run the project easily.
|
41
|
+
|
42
|
+
```bash
|
43
|
+
viewer start
|
44
|
+
viewer start --ui # only start the UI side
|
45
|
+
viewer start --server # only start the server
|
46
|
+
|
47
|
+
viewer stop
|
48
|
+
viewer stop --ui # only stop the UI side
|
49
|
+
viewer stop --server # only stop the server
|
50
|
+
```
|
51
|
+
|
52
|
+
You can also run a simulation from the command line. This is useful for debugging, when you want to run a simulation without the web interface, and also for running simulations that uses a different version of the multimodalsim package.
|
53
|
+
|
54
|
+
Several arguments are available to customize the simulation and can be found with the --help option, but the required arguments will be asked interactively if not provided. The command to run a simulation is:
|
55
|
+
|
56
|
+
```bash
|
57
|
+
viewer simulate
|
58
|
+
```
|
59
|
+
|
60
|
+
## `DataCollector`
|
61
|
+
|
62
|
+
The `SimulationVisualizationDataCollector` class is used to collect data from the simulation and visualize it. You can pass an instance of this class to the simulation to collect data during the simulation. This might be useful if you work on the multimodalsim package and want to visualize the simulation data in real-time.
|
63
|
+
|
64
|
+
## Input data
|
65
|
+
|
66
|
+
To run a simulation, you need to provide input data. You can upload input data folders through the web interface. Some basic input data folders are available [here](https://github.com/lab-core/multimodal-data). You can also clone the repository and use the data from there :
|
67
|
+
|
68
|
+
```bash
|
69
|
+
git clone https://github.com/lab-core/multimodal-data.git
|
70
|
+
```
|
@@ -0,0 +1,37 @@
|
|
1
|
+
# multimodalsim-viewer
|
2
|
+
|
3
|
+
This package provides an interface to the [multimodalsim simulation project](https://pypi.org/project/multimodalsim/), allowing you to run and visualize simulations easily through a web interface.
|
4
|
+
|
5
|
+
## Usage
|
6
|
+
|
7
|
+
You have access to several commands that will allow you to run the project easily.
|
8
|
+
|
9
|
+
```bash
|
10
|
+
viewer start
|
11
|
+
viewer start --ui # only start the UI side
|
12
|
+
viewer start --server # only start the server
|
13
|
+
|
14
|
+
viewer stop
|
15
|
+
viewer stop --ui # only stop the UI side
|
16
|
+
viewer stop --server # only stop the server
|
17
|
+
```
|
18
|
+
|
19
|
+
You can also run a simulation from the command line. This is useful for debugging, when you want to run a simulation without the web interface, and also for running simulations that uses a different version of the multimodalsim package.
|
20
|
+
|
21
|
+
Several arguments are available to customize the simulation and can be found with the --help option, but the required arguments will be asked interactively if not provided. The command to run a simulation is:
|
22
|
+
|
23
|
+
```bash
|
24
|
+
viewer simulate
|
25
|
+
```
|
26
|
+
|
27
|
+
## `DataCollector`
|
28
|
+
|
29
|
+
The `SimulationVisualizationDataCollector` class is used to collect data from the simulation and visualize it. You can pass an instance of this class to the simulation to collect data during the simulation. This might be useful if you work on the multimodalsim package and want to visualize the simulation data in real-time.
|
30
|
+
|
31
|
+
## Input data
|
32
|
+
|
33
|
+
To run a simulation, you need to provide input data. You can upload input data folders through the web interface. Some basic input data folders are available [here](https://github.com/lab-core/multimodal-data). You can also clone the repository and use the data from there :
|
34
|
+
|
35
|
+
```bash
|
36
|
+
git clone https://github.com/lab-core/multimodal-data.git
|
37
|
+
```
|
@@ -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()
|
{multimodalsim_viewer-0.0.1 → multimodalsim_viewer-0.0.2}/multimodalsim_viewer/server/http_routes.py
RENAMED
@@ -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"})
|
@@ -0,0 +1,10 @@
|
|
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")
|