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