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