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