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