multimodalsim-viewer 0.0.1__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.
Files changed (38) hide show
  1. multimodalsim_viewer/__init__.py +0 -0
  2. multimodalsim_viewer/server/__init__.py +0 -0
  3. multimodalsim_viewer/server/http_routes.py +125 -0
  4. multimodalsim_viewer/server/log_manager.py +15 -0
  5. multimodalsim_viewer/server/scripts.py +72 -0
  6. multimodalsim_viewer/server/server.py +210 -0
  7. multimodalsim_viewer/server/server_utils.py +129 -0
  8. multimodalsim_viewer/server/simulation.py +154 -0
  9. multimodalsim_viewer/server/simulation_manager.py +607 -0
  10. multimodalsim_viewer/server/simulation_visualization_data_collector.py +756 -0
  11. multimodalsim_viewer/server/simulation_visualization_data_model.py +1693 -0
  12. multimodalsim_viewer/ui/__init__.py +0 -0
  13. multimodalsim_viewer/ui/cli.py +45 -0
  14. multimodalsim_viewer/ui/server.py +44 -0
  15. multimodalsim_viewer/ui/static/bitmap-fonts/custom-sans-serif.png +0 -0
  16. multimodalsim_viewer/ui/static/bitmap-fonts/custom-sans-serif.xml +1 -0
  17. multimodalsim_viewer/ui/static/chunk-MTC2LSCT.js +1 -0
  18. multimodalsim_viewer/ui/static/chunk-U5CGW4P4.js +7 -0
  19. multimodalsim_viewer/ui/static/favicon.ico +0 -0
  20. multimodalsim_viewer/ui/static/images/control-bar.png +0 -0
  21. multimodalsim_viewer/ui/static/images/sample-bus.png +0 -0
  22. multimodalsim_viewer/ui/static/images/sample-stop.png +0 -0
  23. multimodalsim_viewer/ui/static/images/sample-wait.png +0 -0
  24. multimodalsim_viewer/ui/static/images/simulation-control-bar.png +0 -0
  25. multimodalsim_viewer/ui/static/images/zoom-out-passenger.png +0 -0
  26. multimodalsim_viewer/ui/static/images/zoom-out-vehicle.png +0 -0
  27. multimodalsim_viewer/ui/static/index.html +15 -0
  28. multimodalsim_viewer/ui/static/main-X7OVCS3N.js +3648 -0
  29. multimodalsim_viewer/ui/static/media/layers-2x-TBM42ERR.png +0 -0
  30. multimodalsim_viewer/ui/static/media/layers-55W3Q4RM.png +0 -0
  31. multimodalsim_viewer/ui/static/media/marker-icon-2V3QKKVC.png +0 -0
  32. multimodalsim_viewer/ui/static/polyfills-FFHMD2TL.js +2 -0
  33. multimodalsim_viewer/ui/static/styles-KU7LTPET.css +1 -0
  34. multimodalsim_viewer-0.0.1.dist-info/METADATA +21 -0
  35. multimodalsim_viewer-0.0.1.dist-info/RECORD +38 -0
  36. multimodalsim_viewer-0.0.1.dist-info/WHEEL +5 -0
  37. multimodalsim_viewer-0.0.1.dist-info/entry_points.txt +8 -0
  38. multimodalsim_viewer-0.0.1.dist-info/top_level.txt +1 -0
@@ -0,0 +1,1693 @@
1
+ import json
2
+ import math
3
+ import os
4
+ from enum import Enum
5
+
6
+ import multimodalsim.optimization.dispatcher # To avoid circular import error
7
+ from filelock import FileLock
8
+ from multimodalsim.simulator.environment import Environment
9
+ from multimodalsim.simulator.request import Leg, Trip
10
+ from multimodalsim.simulator.stop import Stop
11
+ from multimodalsim.simulator.vehicle import Route, Vehicle
12
+ from multimodalsim.state_machine.status import PassengerStatus, VehicleStatus
13
+ from multimodalsim_viewer.server.server_utils import (
14
+ SAVE_VERSION,
15
+ SIMULATION_SAVE_FILE_SEPARATOR,
16
+ )
17
+
18
+
19
+ # MARK: Enums
20
+ def convert_passenger_status_to_string(status: PassengerStatus) -> str:
21
+ if status == PassengerStatus.RELEASE:
22
+ return "release"
23
+ elif status == PassengerStatus.ASSIGNED:
24
+ return "assigned"
25
+ elif status == PassengerStatus.READY:
26
+ return "ready"
27
+ elif status == PassengerStatus.ONBOARD:
28
+ return "onboard"
29
+ elif status == PassengerStatus.COMPLETE:
30
+ return "complete"
31
+ else:
32
+ raise ValueError(f"Unknown PassengerStatus {status}")
33
+
34
+
35
+ def convert_vehicle_status_to_string(status: VehicleStatus) -> str:
36
+ if status == VehicleStatus.RELEASE:
37
+ return "release"
38
+ elif status == VehicleStatus.IDLE:
39
+ return "idle"
40
+ elif status == VehicleStatus.BOARDING:
41
+ return "boarding"
42
+ elif status == VehicleStatus.ENROUTE:
43
+ return "enroute"
44
+ elif status == VehicleStatus.ALIGHTING:
45
+ return "alighting"
46
+ elif status == VehicleStatus.COMPLETE:
47
+ return "complete"
48
+ else:
49
+ raise ValueError(f"Unknown VehicleStatus {status}")
50
+
51
+
52
+ def convert_string_to_passenger_status(status: str) -> PassengerStatus:
53
+ if status == "release":
54
+ return PassengerStatus.RELEASE
55
+ elif status == "assigned":
56
+ return PassengerStatus.ASSIGNED
57
+ elif status == "ready":
58
+ return PassengerStatus.READY
59
+ elif status == "onboard":
60
+ return PassengerStatus.ONBOARD
61
+ elif status == "complete":
62
+ return PassengerStatus.COMPLETE
63
+ else:
64
+ raise ValueError(f"Unknown PassengerStatus {status}")
65
+
66
+
67
+ def convert_string_to_vehicle_status(status: str) -> VehicleStatus:
68
+ if status == "release":
69
+ return VehicleStatus.RELEASE
70
+ elif status == "idle":
71
+ return VehicleStatus.IDLE
72
+ elif status == "boarding":
73
+ return VehicleStatus.BOARDING
74
+ elif status == "enroute":
75
+ return VehicleStatus.ENROUTE
76
+ elif status == "alighting":
77
+ return VehicleStatus.ALIGHTING
78
+ elif status == "complete":
79
+ return VehicleStatus.COMPLETE
80
+ else:
81
+ raise ValueError(f"Unknown VehicleStatus {status}")
82
+
83
+
84
+ # MARK: Serializable
85
+ class Serializable:
86
+ def serialize(self) -> dict:
87
+ raise NotImplementedError()
88
+
89
+ @staticmethod
90
+ def deserialize(data: str) -> "Serializable":
91
+ """
92
+ Deserialize a dictionary into an instance of the class.
93
+
94
+ If the dictionary is not valid, return None.
95
+ """
96
+ raise NotImplementedError()
97
+
98
+
99
+ # MARK: Leg
100
+ class VisualizedLeg(Serializable):
101
+ assigned_vehicle_id: str | None
102
+ boarding_stop_index: int | None
103
+ alighting_stop_index: int | None
104
+ boarding_time: float | None
105
+ alighting_time: float | None
106
+ assigned_time: float | None
107
+
108
+ def __init__(
109
+ self,
110
+ assigned_vehicle_id: str | None,
111
+ boarding_stop_index: int | None,
112
+ alighting_stop_index: int | None,
113
+ boarding_time: float | None,
114
+ alighting_time: float | None,
115
+ assigned_time: float | None,
116
+ ) -> None:
117
+ self.assigned_vehicle_id = assigned_vehicle_id
118
+ self.boarding_stop_index = boarding_stop_index
119
+ self.alighting_stop_index = alighting_stop_index
120
+ self.boarding_time = boarding_time
121
+ self.alighting_time = alighting_time
122
+ self.assigned_time = assigned_time
123
+
124
+ @classmethod
125
+ def from_leg_environment_and_trip(
126
+ cls,
127
+ leg: Leg,
128
+ environment: Environment,
129
+ trip: Trip,
130
+ previous_leg: Leg | None = None,
131
+ ) -> "VisualizedLeg":
132
+ boarding_stop_index = None
133
+ alighting_stop_index = None
134
+
135
+ route = (
136
+ environment.get_route_by_vehicle_id(leg.assigned_vehicle.id)
137
+ if leg.assigned_vehicle is not None
138
+ else None
139
+ )
140
+
141
+ all_legs = (
142
+ trip.previous_legs
143
+ + ([trip.current_leg] if trip.current_leg else [])
144
+ + trip.next_legs
145
+ )
146
+
147
+ same_vehicle_leg_index = 0
148
+ for i, other_leg in enumerate(all_legs):
149
+ if other_leg.assigned_vehicle == leg.assigned_vehicle:
150
+ if other_leg == leg:
151
+ break
152
+ else:
153
+ same_vehicle_leg_index += 1
154
+
155
+ if route is not None:
156
+ all_stops = route.previous_stops.copy()
157
+ if route.current_stop is not None:
158
+ all_stops.append(route.current_stop)
159
+ all_stops += route.next_stops
160
+
161
+ trip_found_count = 0
162
+
163
+ for i, stop in enumerate(all_stops):
164
+ if boarding_stop_index is None and trip in (
165
+ stop.passengers_to_board
166
+ + stop.boarding_passengers
167
+ + stop.boarded_passengers
168
+ ):
169
+ if trip_found_count == same_vehicle_leg_index:
170
+ boarding_stop_index = i
171
+ break
172
+ trip_found_count += 1
173
+
174
+ trip_found_count = 0
175
+
176
+ for i, stop in enumerate(all_stops):
177
+ if alighting_stop_index is None and trip in (
178
+ stop.passengers_to_alight
179
+ + stop.alighting_passengers
180
+ + stop.alighted_passengers
181
+ ):
182
+ if trip_found_count == same_vehicle_leg_index:
183
+ alighting_stop_index = i
184
+ break
185
+ trip_found_count += 1
186
+
187
+ assigned_vehicle_id = (
188
+ leg.assigned_vehicle.id if leg.assigned_vehicle is not None else None
189
+ )
190
+
191
+ assigned_time = None
192
+ if assigned_vehicle_id is not None:
193
+ if previous_leg is not None and previous_leg.assigned_time is not None:
194
+ assigned_time = previous_leg.assigned_time
195
+ else:
196
+ assigned_time = environment.current_time
197
+
198
+ return cls(
199
+ assigned_vehicle_id,
200
+ boarding_stop_index,
201
+ alighting_stop_index,
202
+ leg.boarding_time,
203
+ leg.alighting_time,
204
+ assigned_time,
205
+ )
206
+
207
+ def serialize(self) -> dict:
208
+ serialized = {}
209
+
210
+ if self.assigned_vehicle_id is not None:
211
+ serialized["assignedVehicleId"] = self.assigned_vehicle_id
212
+
213
+ if self.boarding_stop_index is not None:
214
+ serialized["boardingStopIndex"] = self.boarding_stop_index
215
+
216
+ if self.alighting_stop_index is not None:
217
+ serialized["alightingStopIndex"] = self.alighting_stop_index
218
+
219
+ if self.boarding_time is not None:
220
+ serialized["boardingTime"] = self.boarding_time
221
+
222
+ if self.alighting_time is not None:
223
+ serialized["alightingTime"] = self.alighting_time
224
+ if self.assigned_time is not None:
225
+ serialized["assignedTime"] = self.assigned_time
226
+
227
+ return serialized
228
+
229
+ @staticmethod
230
+ def deserialize(data: str) -> "VisualizedLeg":
231
+ if isinstance(data, str):
232
+ data = json.loads(data.replace("'", '"'))
233
+
234
+ assigned_vehicle_id = data.get("assignedVehicleId", None)
235
+ boarding_stop_index = data.get("boardingStopIndex", None)
236
+ alighting_stop_index = data.get("alightingStopIndex", None)
237
+ boarding_time = data.get("boardingTime", None)
238
+ alighting_time = data.get("alightingTime", None)
239
+ assigned_time = data.get("assignedTime", None)
240
+
241
+ return VisualizedLeg(
242
+ assigned_vehicle_id,
243
+ boarding_stop_index,
244
+ alighting_stop_index,
245
+ boarding_time,
246
+ alighting_time,
247
+ assigned_time,
248
+ )
249
+
250
+
251
+ # MARK: Passenger
252
+ class VisualizedPassenger(Serializable):
253
+ passenger_id: str
254
+ name: str | None
255
+ status: PassengerStatus
256
+ number_of_passengers: int
257
+
258
+ previous_legs: list[VisualizedLeg]
259
+ current_leg: VisualizedLeg | None
260
+ next_legs: list[VisualizedLeg]
261
+
262
+ def __init__(
263
+ self,
264
+ passenger_id: str,
265
+ name: str | None,
266
+ status: PassengerStatus,
267
+ number_of_passengers: int,
268
+ previous_legs: list[VisualizedLeg],
269
+ current_leg: VisualizedLeg | None,
270
+ next_legs: list[VisualizedLeg],
271
+ ) -> None:
272
+ self.passenger_id = passenger_id
273
+ self.name = name
274
+ self.status = status
275
+ self.number_of_passengers = number_of_passengers
276
+
277
+ self.previous_legs = previous_legs
278
+ self.current_leg = current_leg
279
+ self.next_legs = next_legs
280
+
281
+ @classmethod
282
+ def from_trip_and_environment(
283
+ cls, trip: Trip, environment: Environment
284
+ ) -> "VisualizedPassenger":
285
+ previous_legs = [
286
+ VisualizedLeg.from_leg_environment_and_trip(leg, environment, trip)
287
+ for leg in trip.previous_legs
288
+ ]
289
+ current_leg = (
290
+ VisualizedLeg.from_leg_environment_and_trip(
291
+ trip.current_leg, environment, trip
292
+ )
293
+ if trip.current_leg is not None
294
+ else None
295
+ )
296
+ next_legs = [
297
+ VisualizedLeg.from_leg_environment_and_trip(leg, environment, trip)
298
+ for leg in trip.next_legs
299
+ ]
300
+
301
+ return cls(
302
+ trip.id,
303
+ trip.name,
304
+ trip.status,
305
+ trip.nb_passengers,
306
+ previous_legs,
307
+ current_leg,
308
+ next_legs,
309
+ )
310
+
311
+ def serialize(self) -> dict:
312
+ serialized = {
313
+ "id": self.passenger_id,
314
+ "status": convert_passenger_status_to_string(self.status),
315
+ "numberOfPassengers": self.number_of_passengers,
316
+ }
317
+
318
+ if self.name is not None:
319
+ serialized["name"] = self.name
320
+
321
+ serialized["previousLegs"] = [leg.serialize() for leg in self.previous_legs]
322
+
323
+ if self.current_leg is not None:
324
+ serialized["currentLeg"] = self.current_leg.serialize()
325
+
326
+ serialized["nextLegs"] = [leg.serialize() for leg in self.next_legs]
327
+
328
+ return serialized
329
+
330
+ @staticmethod
331
+ def deserialize(data: str) -> "VisualizedPassenger":
332
+ if isinstance(data, str):
333
+ data = json.loads(data.replace("'", '"'))
334
+
335
+ if (
336
+ "id" not in data
337
+ or "status" not in data
338
+ or "previousLegs" not in data
339
+ or "nextLegs" not in data
340
+ or "numberOfPassengers" not in data
341
+ ):
342
+ raise ValueError("Invalid data for VisualizedPassenger")
343
+
344
+ passenger_id = str(data["id"])
345
+ name = data.get("name", None)
346
+ status = convert_string_to_passenger_status(data["status"])
347
+ number_of_passengers = int(data["numberOfPassengers"])
348
+
349
+ previous_legs = [
350
+ VisualizedLeg.deserialize(leg_data) for leg_data in data["previousLegs"]
351
+ ]
352
+ next_legs = [
353
+ VisualizedLeg.deserialize(leg_data) for leg_data in data["nextLegs"]
354
+ ]
355
+
356
+ current_leg = data.get("currentLeg", None)
357
+ if current_leg is not None:
358
+ current_leg = VisualizedLeg.deserialize(current_leg)
359
+
360
+ return VisualizedPassenger(
361
+ passenger_id,
362
+ name,
363
+ status,
364
+ number_of_passengers,
365
+ previous_legs,
366
+ current_leg,
367
+ next_legs,
368
+ )
369
+
370
+
371
+ # MARK: Stop
372
+ class VisualizedStop(Serializable):
373
+ arrival_time: float
374
+ departure_time: float | None
375
+ latitude: float | None
376
+ longitude: float | None
377
+ capacity: int | None
378
+ label: str
379
+
380
+ def __init__(
381
+ self,
382
+ arrival_time: float,
383
+ departure_time: float,
384
+ latitude: float | None,
385
+ longitude: float | None,
386
+ capacity: int | None,
387
+ label: str,
388
+ ) -> None:
389
+ self.arrival_time = arrival_time
390
+ self.departure_time = departure_time
391
+ self.latitude = latitude
392
+ self.longitude = longitude
393
+ self.capacity = capacity
394
+ self.label = label
395
+
396
+ @classmethod
397
+ def from_stop(cls, stop: Stop) -> "VisualizedStop":
398
+ return cls(
399
+ stop.arrival_time,
400
+ stop.departure_time if stop.departure_time != math.inf else None,
401
+ stop.location.lat,
402
+ stop.location.lon,
403
+ stop.capacity,
404
+ stop.location.label,
405
+ )
406
+
407
+ def serialize(self) -> dict:
408
+ serialized = {"arrivalTime": self.arrival_time}
409
+
410
+ if self.departure_time is not None:
411
+ serialized["departureTime"] = self.departure_time
412
+
413
+ if self.latitude is not None and self.longitude is not None:
414
+ serialized["position"] = {
415
+ "latitude": self.latitude,
416
+ "longitude": self.longitude,
417
+ }
418
+
419
+ if self.capacity is not None:
420
+ serialized["capacity"] = self.capacity
421
+
422
+ serialized["label"] = self.label
423
+
424
+ return serialized
425
+
426
+ @staticmethod
427
+ def deserialize(data: str) -> "VisualizedStop":
428
+ if isinstance(data, str):
429
+ data = json.loads(data.replace("'", '"'))
430
+
431
+ if "arrivalTime" not in data or "label" not in data:
432
+ raise ValueError("Invalid data for VisualizedStop")
433
+
434
+ arrival_time = float(data["arrivalTime"])
435
+ departure_time = data.get("departureTime", None)
436
+
437
+ latitude = None
438
+ longitude = None
439
+
440
+ position = data.get("position", None)
441
+
442
+ if position is not None:
443
+ latitude = position.get("latitude", None)
444
+ longitude = position.get("longitude", None)
445
+
446
+ capacity = data.get("capacity", None)
447
+
448
+ if capacity is not None:
449
+ capacity = int(capacity)
450
+
451
+ label = data["label"]
452
+
453
+ return VisualizedStop(
454
+ arrival_time, departure_time, latitude, longitude, capacity, label
455
+ )
456
+
457
+
458
+ # MARK: Vehicle
459
+ class VisualizedVehicle(Serializable):
460
+ vehicle_id: str
461
+ mode: str | None
462
+ status: VehicleStatus
463
+ polylines: dict[str, tuple[str, list[float]]] | None
464
+ previous_stops: list[VisualizedStop]
465
+ current_stop: VisualizedStop | None
466
+ next_stops: list[VisualizedStop]
467
+ capacity: int
468
+ name: str | None
469
+
470
+ def __init__(
471
+ self,
472
+ vehicle_id: str | int,
473
+ mode: str | None,
474
+ status: VehicleStatus,
475
+ polylines: dict[str, tuple[str, list[float]]] | None,
476
+ previous_stops: list[VisualizedStop],
477
+ current_stop: VisualizedStop | None,
478
+ next_stops: list[VisualizedStop],
479
+ capacity: int,
480
+ name: str | None = None,
481
+ ) -> None:
482
+ self.vehicle_id = str(vehicle_id)
483
+ self.mode = mode
484
+ self.status = status
485
+ self.polylines = polylines
486
+
487
+ self.previous_stops = previous_stops
488
+ self.current_stop = current_stop
489
+ self.next_stops = next_stops
490
+
491
+ self.capacity = capacity
492
+ self.name = name
493
+
494
+ @property
495
+ def all_stops(self) -> list[VisualizedStop]:
496
+ return (
497
+ self.previous_stops
498
+ + ([self.current_stop] if self.current_stop is not None else [])
499
+ + self.next_stops
500
+ )
501
+
502
+ @classmethod
503
+ def from_vehicle_and_route(
504
+ cls, vehicle: Vehicle, route: Route
505
+ ) -> "VisualizedVehicle":
506
+ previous_stops = [
507
+ VisualizedStop.from_stop(stop) for stop in route.previous_stops
508
+ ]
509
+ current_stop = (
510
+ VisualizedStop.from_stop(route.current_stop)
511
+ if route.current_stop is not None
512
+ else None
513
+ )
514
+ next_stops = [VisualizedStop.from_stop(stop) for stop in route.next_stops]
515
+ return cls(
516
+ vehicle.id,
517
+ vehicle.mode,
518
+ vehicle.status,
519
+ vehicle.polylines,
520
+ previous_stops,
521
+ current_stop,
522
+ next_stops,
523
+ vehicle.capacity,
524
+ vehicle.name,
525
+ )
526
+
527
+ def serialize(self) -> dict:
528
+ serialized = {
529
+ "id": self.vehicle_id,
530
+ "status": convert_vehicle_status_to_string(self.status),
531
+ "previousStops": [stop.serialize() for stop in self.previous_stops],
532
+ "nextStops": [stop.serialize() for stop in self.next_stops],
533
+ "capacity": self.capacity,
534
+ "name": self.name,
535
+ }
536
+
537
+ if self.mode is not None:
538
+ serialized["mode"] = self.mode
539
+
540
+ if self.current_stop is not None:
541
+ serialized["currentStop"] = self.current_stop.serialize()
542
+
543
+ return serialized
544
+
545
+ @staticmethod
546
+ def deserialize(data: str | dict) -> "VisualizedVehicle":
547
+ if isinstance(data, str):
548
+ data = json.loads(data.replace("'", '"'))
549
+
550
+ if (
551
+ "id" not in data
552
+ or "status" not in data
553
+ or "previousStops" not in data
554
+ or "nextStops" not in data
555
+ or "capacity" not in data
556
+ or "name" not in data
557
+ ):
558
+ raise ValueError("Invalid data for VisualizedVehicle")
559
+
560
+ vehicle_id = str(data["id"])
561
+ mode = data.get("mode", None)
562
+ status = convert_string_to_vehicle_status(data["status"])
563
+ previous_stops = [
564
+ VisualizedStop.deserialize(stop_data) for stop_data in data["previousStops"]
565
+ ]
566
+ next_stops = [
567
+ VisualizedStop.deserialize(stop_data) for stop_data in data["nextStops"]
568
+ ]
569
+ capacity = int(data["capacity"])
570
+ name = data.get("name", None)
571
+
572
+ current_stop = data.get("currentStop", None)
573
+ if current_stop is not None:
574
+ current_stop = VisualizedStop.deserialize(current_stop)
575
+
576
+ return VisualizedVehicle(
577
+ vehicle_id,
578
+ mode,
579
+ status,
580
+ None,
581
+ previous_stops,
582
+ current_stop,
583
+ next_stops,
584
+ capacity,
585
+ name,
586
+ )
587
+
588
+
589
+ # MARK: Environment
590
+ class VisualizedEnvironment(Serializable):
591
+ passengers: dict[str, VisualizedPassenger]
592
+ vehicles: dict[str, VisualizedVehicle]
593
+ statistic: dict[str, dict[str, dict[str, int]]]
594
+ timestamp: float
595
+ estimated_end_time: float
596
+ order: int
597
+
598
+ def __init__(self) -> None:
599
+ self.passengers = {}
600
+ self.vehicles = {}
601
+ self.timestamp = 0
602
+ self.estimated_end_time = 0
603
+ self.order = 0
604
+ self.statistic = None
605
+
606
+ def add_passenger(self, passenger: VisualizedPassenger) -> None:
607
+ self.passengers[passenger.passenger_id] = passenger
608
+
609
+ def get_passenger(self, passenger_id: str) -> VisualizedPassenger:
610
+ if passenger_id in self.passengers:
611
+ return self.passengers[passenger_id]
612
+ raise ValueError(f"Passenger {passenger_id} not found")
613
+
614
+ def add_vehicle(self, vehicle: VisualizedVehicle) -> None:
615
+ self.vehicles[vehicle.vehicle_id] = vehicle
616
+
617
+ def get_vehicle(self, vehicle_id: str) -> VisualizedVehicle:
618
+ if vehicle_id in self.vehicles:
619
+ return self.vehicles[vehicle_id]
620
+ raise ValueError(f"Vehicle {vehicle_id} not found")
621
+
622
+ def serialize(self) -> dict:
623
+ return {
624
+ "passengers": [
625
+ passenger.serialize() for passenger in self.passengers.values()
626
+ ],
627
+ "vehicles": [vehicle.serialize() for vehicle in self.vehicles.values()],
628
+ "timestamp": self.timestamp,
629
+ "estimatedEndTime": self.estimated_end_time,
630
+ "statistic": self.statistic if self.statistic is not None else {},
631
+ "order": self.order,
632
+ }
633
+
634
+ @staticmethod
635
+ def deserialize(data: str) -> "VisualizedEnvironment":
636
+ if isinstance(data, str):
637
+ data = json.loads(data.replace("'", '"'))
638
+
639
+ if (
640
+ "passengers" not in data
641
+ or "vehicles" not in data
642
+ or "timestamp" not in data
643
+ or "estimatedEndTime" not in data
644
+ or "statistic" not in data
645
+ or "order" not in data
646
+ ):
647
+ raise ValueError("Invalid data for VisualizedEnvironment")
648
+
649
+ environment = VisualizedEnvironment()
650
+ for passenger_data in data["passengers"]:
651
+ passenger = VisualizedPassenger.deserialize(passenger_data)
652
+ environment.add_passenger(passenger)
653
+
654
+ for vehicle_data in data["vehicles"]:
655
+ vehicle = VisualizedVehicle.deserialize(vehicle_data)
656
+ environment.add_vehicle(vehicle)
657
+
658
+ environment.timestamp = data["timestamp"]
659
+ environment.estimated_end_time = data["estimatedEndTime"]
660
+ environment.statistic = data["statistic"]
661
+ environment.order = data["order"]
662
+
663
+ return environment
664
+
665
+
666
+ # MARK: Updates
667
+ class UpdateType(Enum):
668
+ CREATE_PASSENGER = "createPassenger"
669
+ CREATE_VEHICLE = "createVehicle"
670
+ UPDATE_PASSENGER_STATUS = "updatePassengerStatus"
671
+ UPDATE_PASSENGER_LEGS = "updatePassengerLegs"
672
+ UPDATE_VEHICLE_STATUS = "updateVehicleStatus"
673
+ UPDATE_VEHICLE_STOPS = "updateVehicleStops"
674
+ UPDATE_STATISTIC = "updateStatistic"
675
+
676
+
677
+ class StatisticUpdate(Serializable):
678
+ statistic: dict[str, dict[str, dict[str, int]]]
679
+
680
+ def __init__(self, statistic: dict) -> None:
681
+ self.statistic = statistic
682
+
683
+ def serialize(self) -> dict[str, dict[str, dict[str, int]]]:
684
+ return {"statistic": self.statistic}
685
+
686
+ @staticmethod
687
+ def deserialize(data: str) -> "StatisticUpdate":
688
+ if isinstance(data, str):
689
+ data = json.loads(data.replace("'", '"'))
690
+
691
+ if "statistic" not in data:
692
+ raise ValueError("Invalid data for StatisticUpdate")
693
+
694
+ return StatisticUpdate(data.statistic)
695
+
696
+
697
+ class PassengerStatusUpdate(Serializable):
698
+ passenger_id: str
699
+ status: PassengerStatus
700
+
701
+ def __init__(self, passenger_id: str, status: PassengerStatus) -> None:
702
+ self.passenger_id = passenger_id
703
+ self.status = status
704
+
705
+ def from_trip(trip: Trip) -> "PassengerStatusUpdate":
706
+ return PassengerStatusUpdate(trip.id, trip.status)
707
+
708
+ def serialize(self) -> dict:
709
+ return {
710
+ "id": self.passenger_id,
711
+ "status": convert_passenger_status_to_string(self.status),
712
+ }
713
+
714
+ @staticmethod
715
+ def deserialize(data: str) -> "PassengerStatusUpdate":
716
+ if isinstance(data, str):
717
+ data = json.loads(data.replace("'", '"'))
718
+
719
+ if "id" not in data or "status" not in data:
720
+ raise ValueError("Invalid data for PassengerStatusUpdate")
721
+
722
+ passenger_id = str(data["id"])
723
+ status = convert_string_to_passenger_status(data["status"])
724
+ return PassengerStatusUpdate(passenger_id, status)
725
+
726
+
727
+ class PassengerLegsUpdate(Serializable):
728
+ passenger_id: str
729
+ previous_legs: list[VisualizedLeg]
730
+ current_leg: VisualizedLeg | None
731
+ next_legs: list[VisualizedLeg]
732
+
733
+ def __init__(
734
+ self,
735
+ passenger_id: str,
736
+ previous_legs: list[VisualizedLeg],
737
+ current_leg: VisualizedLeg | None,
738
+ next_legs: list[VisualizedLeg],
739
+ ) -> None:
740
+ self.passenger_id = passenger_id
741
+ self.previous_legs = previous_legs
742
+ self.current_leg = current_leg
743
+ self.next_legs = next_legs
744
+
745
+ @classmethod
746
+ def from_trip_environment_and_previous_passenger(
747
+ cls,
748
+ trip: Trip,
749
+ environment: Environment,
750
+ previous_passenger: VisualizedPassenger,
751
+ ) -> "PassengerLegsUpdate":
752
+ all_previous_legs = (
753
+ previous_passenger.previous_legs
754
+ + (
755
+ [previous_passenger.current_leg]
756
+ if previous_passenger.current_leg is not None
757
+ else []
758
+ )
759
+ + previous_passenger.next_legs
760
+ )
761
+ current_index = 0
762
+
763
+ previous_legs = []
764
+ for leg in trip.previous_legs:
765
+ previous_leg = None
766
+ if current_index < len(all_previous_legs):
767
+ previous_leg = all_previous_legs[current_index]
768
+ current_index += 1
769
+ previous_legs.append(
770
+ VisualizedLeg.from_leg_environment_and_trip(
771
+ leg, environment, trip, previous_leg
772
+ )
773
+ )
774
+
775
+ previous_leg = None
776
+ if trip.current_leg is not None and current_index < len(all_previous_legs):
777
+ previous_leg = all_previous_legs[current_index]
778
+ current_index += 1
779
+ current_leg = (
780
+ VisualizedLeg.from_leg_environment_and_trip(
781
+ trip.current_leg, environment, trip, previous_leg
782
+ )
783
+ if trip.current_leg is not None
784
+ else None
785
+ )
786
+
787
+ next_legs = []
788
+ for leg in trip.next_legs:
789
+ next_leg = None
790
+ if current_index < len(all_previous_legs):
791
+ next_leg = all_previous_legs[current_index]
792
+ current_index += 1
793
+ next_legs.append(
794
+ VisualizedLeg.from_leg_environment_and_trip(
795
+ leg, environment, trip, next_leg
796
+ )
797
+ )
798
+
799
+ return cls(trip.id, previous_legs, current_leg, next_legs)
800
+
801
+ def serialize(self) -> dict:
802
+ serialized = {
803
+ "id": self.passenger_id,
804
+ "previousLegs": [leg.serialize() for leg in self.previous_legs],
805
+ "nextLegs": [leg.serialize() for leg in self.next_legs],
806
+ }
807
+
808
+ if self.current_leg is not None:
809
+ serialized["currentLeg"] = self.current_leg.serialize()
810
+
811
+ return serialized
812
+
813
+ @staticmethod
814
+ def deserialize(data: str) -> "PassengerLegsUpdate":
815
+ if isinstance(data, str):
816
+ data = json.loads(data.replace("'", '"'))
817
+
818
+ if "id" not in data or "previousLegs" not in data or "nextLegs" not in data:
819
+ raise ValueError("Invalid data for PassengerLegsUpdate")
820
+
821
+ passenger_id = str(data["id"])
822
+ previous_legs = [
823
+ VisualizedLeg.deserialize(leg_data) for leg_data in data["previousLegs"]
824
+ ]
825
+ next_legs = [
826
+ VisualizedLeg.deserialize(leg_data) for leg_data in data["nextLegs"]
827
+ ]
828
+
829
+ current_leg = data.get("currentLeg", None)
830
+ if current_leg is not None:
831
+ current_leg = VisualizedLeg.deserialize(current_leg)
832
+
833
+ return PassengerLegsUpdate(passenger_id, previous_legs, current_leg, next_legs)
834
+
835
+
836
+ class VehicleStatusUpdate(Serializable):
837
+ vehicle_id: str
838
+ status: VehicleStatus
839
+
840
+ def __init__(self, vehicle_id: str, status: VehicleStatus) -> None:
841
+ self.vehicle_id = vehicle_id
842
+ self.status = status
843
+
844
+ def from_vehicle(vehicle: Vehicle) -> "VehicleStatusUpdate":
845
+ return VehicleStatusUpdate(vehicle.id, vehicle.status)
846
+
847
+ def serialize(self) -> dict:
848
+ return {
849
+ "id": self.vehicle_id,
850
+ "status": convert_vehicle_status_to_string(self.status),
851
+ }
852
+
853
+ @staticmethod
854
+ def deserialize(data: str) -> "VehicleStatusUpdate":
855
+ if isinstance(data, str):
856
+ data = json.loads(data.replace("'", '"'))
857
+
858
+ if "id" not in data or "status" not in data:
859
+ raise ValueError("Invalid data for VehicleStatusUpdate")
860
+
861
+ vehicle_id = str(data["id"])
862
+ status = convert_string_to_vehicle_status(data["status"])
863
+ return VehicleStatusUpdate(vehicle_id, status)
864
+
865
+
866
+ class VehicleStopsUpdate(Serializable):
867
+ vehicle_id: str
868
+ previous_stops: list[VisualizedStop]
869
+ current_stop: VisualizedStop | None
870
+ next_stops: list[VisualizedStop]
871
+
872
+ def __init__(
873
+ self,
874
+ vehicle_id: str,
875
+ previous_stops: list[VisualizedStop],
876
+ current_stop: VisualizedStop | None,
877
+ next_stops: list[VisualizedStop],
878
+ ) -> None:
879
+ self.vehicle_id = vehicle_id
880
+ self.previous_stops = previous_stops
881
+ self.current_stop = current_stop
882
+ self.next_stops = next_stops
883
+
884
+ @classmethod
885
+ def from_vehicle_and_route(
886
+ cls, vehicle: Vehicle, route: Route
887
+ ) -> "VehicleStopsUpdate":
888
+ previous_stops = [
889
+ VisualizedStop.from_stop(stop) for stop in route.previous_stops
890
+ ]
891
+ current_stop = (
892
+ VisualizedStop.from_stop(route.current_stop)
893
+ if route.current_stop is not None
894
+ else None
895
+ )
896
+ next_stops = [VisualizedStop.from_stop(stop) for stop in route.next_stops]
897
+ return cls(vehicle.id, previous_stops, current_stop, next_stops)
898
+
899
+ def serialize(self) -> dict:
900
+ serialized = {
901
+ "id": self.vehicle_id,
902
+ "previousStops": [stop.serialize() for stop in self.previous_stops],
903
+ "nextStops": [stop.serialize() for stop in self.next_stops],
904
+ }
905
+
906
+ if self.current_stop is not None:
907
+ serialized["currentStop"] = self.current_stop.serialize()
908
+
909
+ return serialized
910
+
911
+ @staticmethod
912
+ def deserialize(data: str) -> "VehicleStopsUpdate":
913
+ if isinstance(data, str):
914
+ data = json.loads(data.replace("'", '"'))
915
+
916
+ if "id" not in data or "previousStops" not in data or "nextStops" not in data:
917
+ raise ValueError("Invalid data for VehicleStopsUpdate")
918
+
919
+ vehicle_id = str(data["id"])
920
+ previous_stops = [
921
+ VisualizedStop.deserialize(stop_data) for stop_data in data["previousStops"]
922
+ ]
923
+ next_stops = [
924
+ VisualizedStop.deserialize(stop_data) for stop_data in data["nextStops"]
925
+ ]
926
+
927
+ current_stop = data.get("currentStop", None)
928
+ if current_stop is not None:
929
+ current_stop = VisualizedStop.deserialize(current_stop)
930
+
931
+ return VehicleStopsUpdate(vehicle_id, previous_stops, current_stop, next_stops)
932
+
933
+
934
+ class Update(Serializable):
935
+ type: UpdateType
936
+ data: Serializable
937
+ timestamp: float
938
+ order: int
939
+
940
+ def __init__(
941
+ self,
942
+ type: UpdateType,
943
+ data: Serializable,
944
+ timestamp: float,
945
+ ) -> None:
946
+ self.type = type
947
+ self.data = data
948
+ self.timestamp = timestamp
949
+ self.order = 0
950
+
951
+ def serialize(self) -> dict:
952
+ return {
953
+ "type": self.type.value,
954
+ "data": self.data.serialize(),
955
+ "timestamp": self.timestamp,
956
+ "order": self.order,
957
+ }
958
+
959
+ @staticmethod
960
+ def deserialize(data: str) -> "Update":
961
+ if isinstance(data, str):
962
+ data = json.loads(data.replace("'", '"'))
963
+
964
+ if (
965
+ "type" not in data
966
+ or "data" not in data
967
+ or "timestamp" not in data
968
+ or "order" not in data
969
+ ):
970
+ raise ValueError("Invalid data for Update")
971
+
972
+ update_type = UpdateType(data["type"])
973
+ update_data = data["data"]
974
+ timestamp = float(data["timestamp"])
975
+
976
+ if update_type == UpdateType.CREATE_PASSENGER:
977
+ update_data = VisualizedPassenger.deserialize(update_data)
978
+ elif update_type == UpdateType.CREATE_VEHICLE:
979
+ update_data = VisualizedVehicle.deserialize(update_data)
980
+ elif update_type == UpdateType.UPDATE_PASSENGER_STATUS:
981
+ update_data = PassengerStatusUpdate.deserialize(update_data)
982
+ elif update_type == UpdateType.UPDATE_PASSENGER_LEGS:
983
+ update_data = PassengerLegsUpdate.deserialize(update_data)
984
+ elif update_type == UpdateType.UPDATE_VEHICLE_STATUS:
985
+ update_data = VehicleStatusUpdate.deserialize(update_data)
986
+ elif update_type == UpdateType.UPDATE_VEHICLE_STOPS:
987
+ update_data = VehicleStopsUpdate.deserialize(update_data)
988
+ elif update_type == UpdateType.UPDATE_STATISTIC:
989
+ update_data = StatisticUpdate.deserialize(update_data)
990
+
991
+ update = Update(update_type, update_data, timestamp)
992
+ update.order = data["order"]
993
+ return update
994
+
995
+
996
+ # MARK: State
997
+ class VisualizedState(VisualizedEnvironment):
998
+ updates: list[Update]
999
+
1000
+ def __init__(self) -> None:
1001
+ super().__init__()
1002
+ self.updates = []
1003
+
1004
+ @classmethod
1005
+ def from_environment(cls, environment: VisualizedEnvironment) -> "VisualizedState":
1006
+ state = cls()
1007
+ state.passengers = environment.passengers
1008
+ state.vehicles = environment.vehicles
1009
+ state.timestamp = environment.timestamp
1010
+ state.estimated_end_time = environment.estimated_end_time
1011
+ state.order = environment.order
1012
+ return state
1013
+
1014
+ def serialize(self) -> dict:
1015
+ serialized = super().serialize()
1016
+ serialized["updates"] = [update.serialize() for update in self.updates]
1017
+ return serialized
1018
+
1019
+ @staticmethod
1020
+ def deserialize(data: str) -> "VisualizedState":
1021
+ if isinstance(data, str):
1022
+ data = json.loads(data.replace("'", '"'))
1023
+
1024
+ if "updates" not in data:
1025
+ raise ValueError("Invalid data for VisualizedState")
1026
+
1027
+ environment = VisualizedEnvironment.deserialize(data)
1028
+
1029
+ state = VisualizedState()
1030
+ state.passengers = environment.passengers
1031
+ state.vehicles = environment.vehicles
1032
+ state.timestamp = environment.timestamp
1033
+ state.estimated_end_time = environment.estimated_end_time
1034
+ state.order = environment.order
1035
+
1036
+ for update_data in data["updates"]:
1037
+ update = Update.deserialize(update_data)
1038
+ state.updates.append(update)
1039
+
1040
+ return state
1041
+
1042
+
1043
+ # MARK: Simulation Information
1044
+ class SimulationInformation(Serializable):
1045
+ version: int
1046
+ simulation_id: str
1047
+ name: str
1048
+ start_time: str
1049
+ data: str
1050
+ simulation_start_time: float | None
1051
+ simulation_end_time: float | None
1052
+ last_update_order: int | None
1053
+
1054
+ def __init__(
1055
+ self,
1056
+ simulation_id: str,
1057
+ data: str,
1058
+ simulation_start_time: str | None,
1059
+ simulation_end_time: str | None,
1060
+ last_update_order: int | None,
1061
+ version: int | None,
1062
+ ) -> None:
1063
+ self.version = version
1064
+ if self.version is None:
1065
+ self.version = SAVE_VERSION
1066
+
1067
+ self.simulation_id = simulation_id
1068
+
1069
+ self.name = simulation_id.split(SIMULATION_SAVE_FILE_SEPARATOR)[1]
1070
+ self.start_time = simulation_id.split(SIMULATION_SAVE_FILE_SEPARATOR)[0]
1071
+ self.data = data
1072
+
1073
+ self.simulation_start_time = simulation_start_time
1074
+ self.simulation_end_time = simulation_end_time
1075
+ self.last_update_order = last_update_order
1076
+
1077
+ def serialize(self) -> dict:
1078
+ serialized = {
1079
+ "version": self.version,
1080
+ "simulationId": self.simulation_id,
1081
+ "name": self.name,
1082
+ "startTime": self.start_time,
1083
+ "data": self.data,
1084
+ }
1085
+ if self.simulation_start_time is not None:
1086
+ serialized["simulationStartTime"] = self.simulation_start_time
1087
+ if self.simulation_end_time is not None:
1088
+ serialized["simulationEndTime"] = self.simulation_end_time
1089
+ if self.last_update_order is not None:
1090
+ serialized["lastUpdateOrder"] = self.last_update_order
1091
+ return serialized
1092
+
1093
+ @staticmethod
1094
+ def deserialize(data: str) -> "SimulationInformation":
1095
+ if isinstance(data, str):
1096
+ data = json.loads(data.replace("'", '"'))
1097
+
1098
+ if "version" not in data or "simulationId" not in data:
1099
+ raise ValueError("Invalid data for SimulationInformation")
1100
+
1101
+ version = int(data["version"])
1102
+ simulation_id = str(data["simulationId"])
1103
+ simulation_data = str(data["data"])
1104
+
1105
+ simulation_start_time = data.get("simulationStartTime", None)
1106
+ simulation_end_time = data.get("simulationEndTime", None)
1107
+ last_update_order = data.get("lastUpdateOrder", None)
1108
+
1109
+ return SimulationInformation(
1110
+ simulation_id,
1111
+ simulation_data,
1112
+ simulation_start_time,
1113
+ simulation_end_time,
1114
+ last_update_order,
1115
+ version,
1116
+ )
1117
+
1118
+
1119
+ # TODO Send it to client
1120
+ # def get_size(start_path: str) -> int:
1121
+ # total_size = 0
1122
+ # for directory_path, _, file_names in os.walk(start_path):
1123
+ # for file_name in file_names:
1124
+ # file_path = os.path.join(directory_path, file_name)
1125
+ # total_size += os.path.getsize(file_path)
1126
+ # return total_size
1127
+
1128
+
1129
+ # MARK: SVDM
1130
+ class SimulationVisualizationDataManager:
1131
+ """
1132
+ This class manage reads and writes of simulation data for visualization.
1133
+ """
1134
+
1135
+ __CORRUPTED_FILE_NAME = ".corrupted"
1136
+ __SAVED_SIMULATIONS_DIRECTORY_NAME = "saved_simulations"
1137
+ __SIMULATION_INFORMATION_FILE_NAME = "simulation_information.json"
1138
+ __STATES_DIRECTORY_NAME = "states"
1139
+ __POLYLINES_DIRECTORY_NAME = "polylines"
1140
+ __POLYLINES_FILE_NAME = "polylines"
1141
+ __POLYLINES_VERSION_FILE_NAME = "version"
1142
+
1143
+ __STATES_ORDER_MINIMUM_LENGTH = 8
1144
+ __STATES_TIMESTAMP_MINIMUM_LENGTH = 8
1145
+
1146
+ # Only send a maximum of __MAX_STATES_AT_ONCE states at once
1147
+ # This should be at least 2
1148
+ __MAX_STATES_AT_ONCE = 2
1149
+
1150
+ # The client keeps a maximum of __MAX_STATES_IN_CLIENT_BEFORE_NECESSARY + __MAX_STATES_IN_CLIENT_AFTER_NECESSARY + 1
1151
+ # states in memory
1152
+ # The current one, the previous __MAX_STATES_IN_CLIENT_BEFORE_NECESSARY and the next __MAX_STATES_IN_CLIENT_AFTER_NECESSARY
1153
+ # __MAX_STATES_IN_CLIENT_BEFORE_NECESSARY = 24
1154
+ # __MAX_STATES_IN_CLIENT_AFTER_NECESSARY = 50
1155
+
1156
+ # MARK: +- Format
1157
+ @staticmethod
1158
+ def __format_json_readable(data: dict, file: str) -> str:
1159
+ return json.dump(data, file, indent=2, separators=(",", ": "), sort_keys=True)
1160
+
1161
+ @staticmethod
1162
+ def __format_json_one_line(data: dict, file: str) -> str:
1163
+ # Add new line before if not empty
1164
+ if file.tell() != 0:
1165
+ file.write("\n")
1166
+ return json.dump(data, file, separators=(",", ":"))
1167
+
1168
+ # MARK: +- File paths
1169
+ @staticmethod
1170
+ def get_saved_simulations_directory_path() -> str:
1171
+ current_directory = os.path.dirname(os.path.abspath(__file__))
1172
+ directory_path = f"{current_directory}/{SimulationVisualizationDataManager.__SAVED_SIMULATIONS_DIRECTORY_NAME}"
1173
+
1174
+ if not os.path.exists(directory_path):
1175
+ os.makedirs(directory_path)
1176
+
1177
+ return directory_path
1178
+
1179
+ @staticmethod
1180
+ def get_all_saved_simulation_ids() -> list[str]:
1181
+ directory_path = (
1182
+ SimulationVisualizationDataManager.get_saved_simulations_directory_path()
1183
+ )
1184
+ return [simulation_id for simulation_id in os.listdir(directory_path)]
1185
+
1186
+ @staticmethod
1187
+ def get_saved_simulation_directory_path(simulation_id: str) -> str:
1188
+ directory_path = (
1189
+ SimulationVisualizationDataManager.get_saved_simulations_directory_path()
1190
+ )
1191
+ simulation_directory_path = f"{directory_path}/{simulation_id}"
1192
+
1193
+ if not os.path.exists(simulation_directory_path):
1194
+ os.makedirs(simulation_directory_path)
1195
+
1196
+ return simulation_directory_path
1197
+
1198
+ # MARK: +- Corrupted
1199
+ @staticmethod
1200
+ def is_simulation_corrupted(simulation_id: str) -> bool:
1201
+ simulation_directory_path = (
1202
+ SimulationVisualizationDataManager.get_saved_simulation_directory_path(
1203
+ simulation_id
1204
+ )
1205
+ )
1206
+
1207
+ return os.path.exists(
1208
+ f"{simulation_directory_path}/{SimulationVisualizationDataManager.__CORRUPTED_FILE_NAME}"
1209
+ )
1210
+
1211
+ @staticmethod
1212
+ def mark_simulation_as_corrupted(simulation_id: str) -> None:
1213
+ simulation_directory_path = (
1214
+ SimulationVisualizationDataManager.get_saved_simulation_directory_path(
1215
+ simulation_id
1216
+ )
1217
+ )
1218
+
1219
+ file_path = f"{simulation_directory_path}/{SimulationVisualizationDataManager.__CORRUPTED_FILE_NAME}"
1220
+
1221
+ with open(file_path, "w") as file:
1222
+ file.write("")
1223
+
1224
+ # MARK: +- Simulation Information
1225
+ @staticmethod
1226
+ def get_saved_simulation_information_file_path(simulation_id: str) -> str:
1227
+ simulation_directory_path = (
1228
+ SimulationVisualizationDataManager.get_saved_simulation_directory_path(
1229
+ simulation_id
1230
+ )
1231
+ )
1232
+ file_path = f"{simulation_directory_path}/{SimulationVisualizationDataManager.__SIMULATION_INFORMATION_FILE_NAME}"
1233
+
1234
+ if not os.path.exists(file_path):
1235
+ with open(file_path, "w") as file:
1236
+ file.write("")
1237
+
1238
+ return file_path
1239
+
1240
+ @staticmethod
1241
+ def set_simulation_information(
1242
+ simulation_id: str, simulation_information: SimulationInformation
1243
+ ) -> None:
1244
+ file_path = SimulationVisualizationDataManager.get_saved_simulation_information_file_path(
1245
+ simulation_id
1246
+ )
1247
+
1248
+ lock = FileLock(f"{file_path}.lock")
1249
+
1250
+ with lock:
1251
+ with open(file_path, "w") as file:
1252
+ SimulationVisualizationDataManager.__format_json_readable(
1253
+ simulation_information.serialize(), file
1254
+ )
1255
+
1256
+ @staticmethod
1257
+ def get_simulation_information(simulation_id: str) -> SimulationInformation:
1258
+ file_path = SimulationVisualizationDataManager.get_saved_simulation_information_file_path(
1259
+ simulation_id
1260
+ )
1261
+
1262
+ lock = FileLock(f"{file_path}.lock")
1263
+
1264
+ with lock:
1265
+ with open(file_path, "r") as file:
1266
+ data = file.read()
1267
+ return SimulationInformation.deserialize(data)
1268
+
1269
+ # MARK: +- States and updates
1270
+ @staticmethod
1271
+ def get_saved_simulation_states_folder_path(simulation_id: str) -> str:
1272
+ simulation_directory_path = (
1273
+ SimulationVisualizationDataManager.get_saved_simulation_directory_path(
1274
+ simulation_id
1275
+ )
1276
+ )
1277
+ folder_path = f"{simulation_directory_path}/{SimulationVisualizationDataManager.__STATES_DIRECTORY_NAME}"
1278
+
1279
+ if not os.path.exists(folder_path):
1280
+ os.makedirs(folder_path)
1281
+
1282
+ return folder_path
1283
+
1284
+ @staticmethod
1285
+ def get_saved_simulation_state_file_path(
1286
+ simulation_id: str, order: int, timestamp: float
1287
+ ) -> str:
1288
+ folder_path = (
1289
+ SimulationVisualizationDataManager.get_saved_simulation_states_folder_path(
1290
+ simulation_id
1291
+ )
1292
+ )
1293
+
1294
+ padded_order = str(order).zfill(
1295
+ SimulationVisualizationDataManager.__STATES_ORDER_MINIMUM_LENGTH
1296
+ )
1297
+ padded_timestamp = str(int(timestamp)).zfill(
1298
+ SimulationVisualizationDataManager.__STATES_TIMESTAMP_MINIMUM_LENGTH
1299
+ )
1300
+
1301
+ # States and updates are stored in a .jsonl file to speed up reads and writes
1302
+ # Each line is a state (the first line) or an update (the following lines)
1303
+ file_path = f"{folder_path}/{padded_order}-{padded_timestamp}.jsonl"
1304
+
1305
+ if not os.path.exists(file_path):
1306
+ with open(file_path, "w") as file:
1307
+ file.write("")
1308
+
1309
+ return file_path
1310
+
1311
+ @staticmethod
1312
+ def get_sorted_states(simulation_id: str) -> list[tuple[int, float]]:
1313
+ folder_path = (
1314
+ SimulationVisualizationDataManager.get_saved_simulation_states_folder_path(
1315
+ simulation_id
1316
+ )
1317
+ )
1318
+
1319
+ all_states_files = [
1320
+ path for path in os.listdir(folder_path) if path.endswith(".jsonl")
1321
+ ] # Filter out lock files
1322
+
1323
+ states = []
1324
+ for state_file in all_states_files:
1325
+ order, timestamp = state_file.split("-")
1326
+ states.append((int(order), float(timestamp.split(".")[0])))
1327
+
1328
+ return sorted(states, key=lambda x: (x[1], x[0]))
1329
+
1330
+ @staticmethod
1331
+ def save_state(simulation_id: str, environment: VisualizedEnvironment) -> str:
1332
+ file_path = (
1333
+ SimulationVisualizationDataManager.get_saved_simulation_state_file_path(
1334
+ simulation_id, environment.order, environment.timestamp
1335
+ )
1336
+ )
1337
+
1338
+ lock = FileLock(f"{file_path}.lock")
1339
+
1340
+ with lock:
1341
+ with open(file_path, "w") as file:
1342
+ SimulationVisualizationDataManager.__format_json_one_line(
1343
+ environment.serialize(), file
1344
+ )
1345
+
1346
+ return file_path
1347
+
1348
+ @staticmethod
1349
+ def save_update(file_path: str, update: Update) -> None:
1350
+ lock = FileLock(f"{file_path}.lock")
1351
+ with lock:
1352
+ with open(file_path, "a") as file:
1353
+ SimulationVisualizationDataManager.__format_json_one_line(
1354
+ update.serialize(), file
1355
+ )
1356
+
1357
+ @staticmethod
1358
+ def get_missing_states(
1359
+ simulation_id: str,
1360
+ visualization_time: float,
1361
+ loaded_state_orders: list[int],
1362
+ is_simulation_complete: bool,
1363
+ ) -> tuple[list[str], dict[list[str]], list[int], bool, int, int, int]:
1364
+ sorted_states = SimulationVisualizationDataManager.get_sorted_states(
1365
+ simulation_id
1366
+ )
1367
+
1368
+ if len(sorted_states) == 0:
1369
+ return ([], {}, [], False, 0, 0, 0)
1370
+
1371
+ necessary_state_index = None
1372
+
1373
+ for index, (order, state_timestamp) in enumerate(sorted_states):
1374
+ if necessary_state_index is None and state_timestamp > visualization_time:
1375
+ necessary_state_index = index
1376
+ break
1377
+
1378
+ if necessary_state_index is None:
1379
+ # If the visualization time is after the last state then
1380
+ # The last state is necessary
1381
+ necessary_state_index = len(sorted_states) - 1
1382
+ else:
1383
+ # Else we need the state before the first state with greater timestamp
1384
+ necessary_state_index -= 1
1385
+
1386
+ # Handle negative indexes
1387
+ necessary_state_index = max(0, necessary_state_index)
1388
+
1389
+ state_orders_to_keep = []
1390
+ missing_states = []
1391
+ missing_updates = {}
1392
+
1393
+ last_state_index_in_client = -1
1394
+ all_state_indexes_in_client = []
1395
+
1396
+ # We want to load the necessary state first, followed by
1397
+ # the __MAX_STATES_IN_CLIENT_AFTER_NECESSARY next states and
1398
+ # then the __MAX_STATES_IN_CLIENT_BEFORE_NECESSARY previous states
1399
+ indexes_to_load = (
1400
+ [necessary_state_index]
1401
+ # + [
1402
+ # next_state_index
1403
+ # for next_state_index in range(
1404
+ # necessary_state_index + 1,
1405
+ # min(
1406
+ # necessary_state_index
1407
+ # + SimulationVisualizationDataManager.__MAX_STATES_IN_CLIENT_AFTER_NECESSARY
1408
+ # + 1,
1409
+ # len(sorted_states),
1410
+ # ),
1411
+ # )
1412
+ # ]
1413
+ # + [
1414
+ # previous_state_index
1415
+ # for previous_state_index in range(
1416
+ # necessary_state_index - 1,
1417
+ # max(
1418
+ # necessary_state_index
1419
+ # - SimulationVisualizationDataManager.__MAX_STATES_IN_CLIENT_BEFORE_NECESSARY
1420
+ # - 1,
1421
+ # -1,
1422
+ # ),
1423
+ # -1,
1424
+ # )
1425
+ # ]
1426
+ # All next states
1427
+ + [
1428
+ next_state_index
1429
+ for next_state_index in range(
1430
+ necessary_state_index + 1, len(sorted_states)
1431
+ )
1432
+ ]
1433
+ # All previous states
1434
+ + [
1435
+ previous_state_index
1436
+ for previous_state_index in range(necessary_state_index - 1, -1, -1)
1437
+ ]
1438
+ )
1439
+
1440
+ for index in indexes_to_load:
1441
+ order, state_timestamp = sorted_states[index]
1442
+
1443
+ # If the client already has the state, skip it
1444
+ # except the last state that might have changed
1445
+ if order in loaded_state_orders and not order == max(loaded_state_orders):
1446
+ state_orders_to_keep.append(order)
1447
+
1448
+ all_state_indexes_in_client.append(index)
1449
+ if index > last_state_index_in_client:
1450
+ last_state_index_in_client = index
1451
+
1452
+ continue
1453
+
1454
+ # Don't add states if the max number of states is reached
1455
+ # but continue the loop to know which states need to be kept
1456
+ if (
1457
+ len(missing_states)
1458
+ >= SimulationVisualizationDataManager.__MAX_STATES_AT_ONCE
1459
+ ):
1460
+ continue
1461
+
1462
+ state_file_path = (
1463
+ SimulationVisualizationDataManager.get_saved_simulation_state_file_path(
1464
+ simulation_id, order, state_timestamp
1465
+ )
1466
+ )
1467
+
1468
+ lock = FileLock(f"{state_file_path}.lock")
1469
+
1470
+ with lock:
1471
+ with open(state_file_path, "r") as file:
1472
+ environment_data = file.readline()
1473
+ missing_states.append(environment_data)
1474
+
1475
+ updates_data = file.readlines()
1476
+ current_state_updates = []
1477
+ for update_data in updates_data:
1478
+ current_state_updates.append(update_data)
1479
+
1480
+ missing_updates[order] = current_state_updates
1481
+
1482
+ all_state_indexes_in_client.append(index)
1483
+ if index > last_state_index_in_client:
1484
+ last_state_index_in_client = index
1485
+
1486
+ client_has_last_state = last_state_index_in_client == len(sorted_states) - 1
1487
+ client_has_max_states = len(missing_states) + len(state_orders_to_keep) >= len(
1488
+ indexes_to_load
1489
+ )
1490
+
1491
+ should_request_more_states = (
1492
+ is_simulation_complete and not client_has_max_states
1493
+ ) or (
1494
+ not is_simulation_complete
1495
+ and (client_has_last_state or not client_has_max_states)
1496
+ )
1497
+
1498
+ first_continuous_state_index = necessary_state_index
1499
+ last_continuous_state_index = necessary_state_index
1500
+
1501
+ all_state_indexes_in_client.sort()
1502
+
1503
+ necessary_state_index_index = all_state_indexes_in_client.index(
1504
+ necessary_state_index
1505
+ )
1506
+
1507
+ for index in range(necessary_state_index_index - 1, -1, -1):
1508
+ if all_state_indexes_in_client[index] == first_continuous_state_index - 1:
1509
+ first_continuous_state_index -= 1
1510
+ else:
1511
+ break
1512
+
1513
+ for index in range(
1514
+ necessary_state_index_index + 1, len(all_state_indexes_in_client)
1515
+ ):
1516
+ if all_state_indexes_in_client[index] == last_continuous_state_index + 1:
1517
+ last_continuous_state_index += 1
1518
+ else:
1519
+ break
1520
+
1521
+ first_continuous_state_order = sorted_states[first_continuous_state_index][0]
1522
+ last_continuous_state_order = sorted_states[last_continuous_state_index][0]
1523
+
1524
+ necessary_state_order = sorted_states[necessary_state_index][0]
1525
+
1526
+ return (
1527
+ missing_states,
1528
+ missing_updates,
1529
+ state_orders_to_keep,
1530
+ should_request_more_states,
1531
+ first_continuous_state_order,
1532
+ last_continuous_state_order,
1533
+ necessary_state_order,
1534
+ )
1535
+
1536
+ # MARK: +- Polylines
1537
+
1538
+ # The polylines are saved with the following structure :
1539
+ # polylines/
1540
+ # version
1541
+ # polylines.jsonl
1542
+ # { "coordinatesString": "string", "encodedPolyline": "string", "coefficients": [float] }
1543
+
1544
+ @staticmethod
1545
+ def get_saved_simulation_polylines_lock(simulation_id: str) -> FileLock:
1546
+ simulation_directory_path = (
1547
+ SimulationVisualizationDataManager.get_saved_simulation_directory_path(
1548
+ simulation_id
1549
+ )
1550
+ )
1551
+ return FileLock(f"{simulation_directory_path}/polylines.lock")
1552
+
1553
+ @staticmethod
1554
+ def get_saved_simulation_polylines_directory_path(simulation_id: str) -> str:
1555
+ simulation_directory_path = (
1556
+ SimulationVisualizationDataManager.get_saved_simulation_directory_path(
1557
+ simulation_id
1558
+ )
1559
+ )
1560
+ directory_path = f"{simulation_directory_path}/{SimulationVisualizationDataManager.__POLYLINES_DIRECTORY_NAME}"
1561
+
1562
+ if not os.path.exists(directory_path):
1563
+ os.makedirs(directory_path)
1564
+
1565
+ return directory_path
1566
+
1567
+ @staticmethod
1568
+ def get_saved_simulation_polylines_version_file_path(simulation_id: str) -> str:
1569
+ directory_path = SimulationVisualizationDataManager.get_saved_simulation_polylines_directory_path(
1570
+ simulation_id
1571
+ )
1572
+ file_path = f"{directory_path}/{SimulationVisualizationDataManager.__POLYLINES_VERSION_FILE_NAME}"
1573
+
1574
+ if not os.path.exists(file_path):
1575
+ with open(file_path, "w") as file:
1576
+ file.write(str(0))
1577
+
1578
+ return file_path
1579
+
1580
+ @staticmethod
1581
+ def set_polylines_version(simulation_id: str, version: int) -> None:
1582
+ """
1583
+ Should always be called in a lock.
1584
+ """
1585
+ file_path = SimulationVisualizationDataManager.get_saved_simulation_polylines_version_file_path(
1586
+ simulation_id
1587
+ )
1588
+
1589
+ with open(file_path, "w") as file:
1590
+ file.write(str(version))
1591
+
1592
+ @staticmethod
1593
+ def get_polylines_version(simulation_id: str) -> int:
1594
+ """
1595
+ Should always be called in a lock.
1596
+ """
1597
+ file_path = SimulationVisualizationDataManager.get_saved_simulation_polylines_version_file_path(
1598
+ simulation_id
1599
+ )
1600
+
1601
+ with open(file_path, "r") as file:
1602
+ return int(file.read())
1603
+
1604
+ @staticmethod
1605
+ def get_polylines_version_with_lock(simulation_id: str) -> int:
1606
+ lock = SimulationVisualizationDataManager.get_saved_simulation_polylines_lock(
1607
+ simulation_id
1608
+ )
1609
+ with lock:
1610
+ return SimulationVisualizationDataManager.get_polylines_version(
1611
+ simulation_id
1612
+ )
1613
+
1614
+ @staticmethod
1615
+ def get_saved_simulation_polylines_file_path(simulation_id: str) -> str:
1616
+ directory_path = SimulationVisualizationDataManager.get_saved_simulation_polylines_directory_path(
1617
+ simulation_id
1618
+ )
1619
+
1620
+ file_path = f"{directory_path}/{SimulationVisualizationDataManager.__POLYLINES_FILE_NAME}.jsonl"
1621
+
1622
+ if not os.path.exists(file_path):
1623
+ with open(file_path, "w") as file:
1624
+ file.write("")
1625
+
1626
+ return file_path
1627
+
1628
+ @staticmethod
1629
+ def set_polylines(
1630
+ simulation_id: str, polylines: dict[str, tuple[str, list[float]]]
1631
+ ) -> None:
1632
+
1633
+ file_path = (
1634
+ SimulationVisualizationDataManager.get_saved_simulation_polylines_file_path(
1635
+ simulation_id
1636
+ )
1637
+ )
1638
+
1639
+ lock = SimulationVisualizationDataManager.get_saved_simulation_polylines_lock(
1640
+ simulation_id
1641
+ )
1642
+
1643
+ with lock:
1644
+ # Increment the version to notify the client that the polylines have changed
1645
+ version = SimulationVisualizationDataManager.get_polylines_version(
1646
+ simulation_id
1647
+ )
1648
+ version += 1
1649
+ SimulationVisualizationDataManager.set_polylines_version(
1650
+ simulation_id, version
1651
+ )
1652
+
1653
+ with open(file_path, "a") as file:
1654
+ for coordinates_string, (
1655
+ encoded_polyline,
1656
+ coefficients,
1657
+ ) in polylines.items():
1658
+ data = {
1659
+ "coordinatesString": coordinates_string,
1660
+ "encodedPolyline": encoded_polyline,
1661
+ "coefficients": coefficients,
1662
+ }
1663
+ SimulationVisualizationDataManager.__format_json_one_line(
1664
+ data, file
1665
+ )
1666
+
1667
+ @staticmethod
1668
+ def get_polylines(
1669
+ simulation_id: str,
1670
+ ) -> tuple[list[str], int]:
1671
+
1672
+ polylines = []
1673
+
1674
+ lock = SimulationVisualizationDataManager.get_saved_simulation_polylines_lock(
1675
+ simulation_id
1676
+ )
1677
+
1678
+ version = 0
1679
+
1680
+ with lock:
1681
+ version = SimulationVisualizationDataManager.get_polylines_version(
1682
+ simulation_id
1683
+ )
1684
+
1685
+ file_path = SimulationVisualizationDataManager.get_saved_simulation_polylines_file_path(
1686
+ simulation_id
1687
+ )
1688
+
1689
+ with open(file_path, "r") as file:
1690
+ for line in file:
1691
+ polylines.append(line)
1692
+
1693
+ return polylines, version