Skip to content

Commit b5de909

Browse files
Merge pull request #15 from nextmv-io/merschformann/better-nextroute-format-compatibility
Improving compatibility with nextroute output
2 parents e73a47b + 689fe65 commit b5de909

File tree

1 file changed

+77
-42
lines changed

1 file changed

+77
-42
lines changed

nextplot/route.py

Lines changed: 77 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
1+
import dataclasses
12
import json
3+
from collections.abc import Callable
24

35
import folium
6+
import jsonpath_ng
47
import plotly.graph_objects as go
58
from folium import plugins
69

@@ -14,28 +17,19 @@
1417
# ==================== Pre-configured plot profiles
1518

1619

20+
@dataclasses.dataclass
1721
class RoutePlotProfile:
1822
"""
1923
Pre-configured plot profiles for routes.
2024
"""
2125

22-
def __init__(
23-
self,
24-
jpath_route: str = "",
25-
jpath_pos: str = "",
26-
jpath_x: str = "",
27-
jpath_y: str = "",
28-
jpath_unassigned: str = "",
29-
jpath_unassigned_x: str = "",
30-
jpath_unassigned_y: str = "",
31-
):
32-
self.jpath_route = jpath_route
33-
self.jpath_pos = jpath_pos
34-
self.jpath_x = jpath_x
35-
self.jpath_y = jpath_y
36-
self.jpath_unassigned = jpath_unassigned
37-
self.jpath_unassigned_x = jpath_unassigned_x
38-
self.jpath_unassigned_y = jpath_unassigned_y
26+
jpath_route: str = ""
27+
jpath_pos: str = ""
28+
jpath_x: str = ""
29+
jpath_y: str = ""
30+
jpath_unassigned: str = ""
31+
jpath_unassigned_x: str = ""
32+
jpath_unassigned_y: str = ""
3933

4034
def __str__(self):
4135
return (
@@ -50,6 +44,28 @@ def __str__(self):
5044
)
5145

5246

47+
@dataclasses.dataclass
48+
class MultiRoutePlotProfile:
49+
"""
50+
Multiple pre-configured plot profiles selected via given input tests.
51+
"""
52+
53+
profiles: list[tuple[Callable[[dict, dict], bool], RoutePlotProfile]] = dataclasses.field(default_factory=list)
54+
fail_message: str = "No suitable profile found for plotting."
55+
56+
def unwrap(self, content_route: dict, content_pos: dict) -> RoutePlotProfile:
57+
"""
58+
Tests the given data against the profiles and returns the first matching profile.
59+
"""
60+
for test, profile in self.profiles:
61+
if test(content_route, content_pos):
62+
return profile
63+
raise Exception(self.fail_message)
64+
65+
def __str__(self):
66+
return "MultiRoutePlotProfile(" + ", ".join([f"{p[0]}: {p[1]}" for p in self.profiles]) + ")"
67+
68+
5369
# ==================== Route mode argument definition
5470

5571

@@ -219,21 +235,26 @@ def arguments(parser):
219235

220236
def parse(
221237
input_route: str,
222-
jpath_route: str,
223-
jpath_unassigned: str,
224-
jpath_unassigned_x: str,
225-
jpath_unassigned_y: str,
226238
input_pos: str,
227-
jpath_pos: str,
228-
jpath_x: str,
229-
jpath_y: str,
239+
profile: MultiRoutePlotProfile | RoutePlotProfile,
230240
) -> tuple[list[list[types.Position]], list[list[types.Position]]]:
231241
"""
232242
Parses the route data from the file(s).
233243
"""
234244
# Load json data
235245
content_route, content_pos = common.load_data(input_route, input_pos)
236246

247+
# Dynamically set profile, if given
248+
if isinstance(profile, MultiRoutePlotProfile):
249+
profile = profile.unwrap(json.loads(content_route), json.loads(content_pos))
250+
jpath_route = profile.jpath_route
251+
jpath_pos = profile.jpath_pos
252+
jpath_x = profile.jpath_x
253+
jpath_y = profile.jpath_y
254+
jpath_unassigned = profile.jpath_unassigned
255+
jpath_unassigned_x = profile.jpath_unassigned_x
256+
jpath_unassigned_y = profile.jpath_unassigned_y
257+
237258
# Extract routes
238259
points = common.extract_position_groups(
239260
content_route,
@@ -553,17 +574,7 @@ def plot(
553574
profile = nextroute_profile()
554575

555576
# Parse data
556-
points, unassigned = parse(
557-
input_route,
558-
profile.jpath_route,
559-
profile.jpath_unassigned,
560-
profile.jpath_unassigned_x,
561-
profile.jpath_unassigned_y,
562-
input_pos,
563-
profile.jpath_pos,
564-
profile.jpath_x,
565-
profile.jpath_y,
566-
)
577+
points, unassigned = parse(input_route, input_pos, profile)
567578

568579
# Quit on no points
569580
if len(points) <= 0:
@@ -691,13 +702,37 @@ def nextroute_profile() -> RoutePlotProfile:
691702
"""
692703
Returns the nextroute profile.
693704
"""
694-
return RoutePlotProfile(
695-
jpath_route="solutions[-1].vehicles[*].route",
696-
jpath_x="stop.location.lon",
697-
jpath_y="stop.location.lat",
698-
jpath_unassigned="solutions[-1].unplanned[*]",
699-
jpath_unassigned_x="location.lon",
700-
jpath_unassigned_y="location.lat",
705+
base_paths = [
706+
"solutions[-1]",
707+
"solution",
708+
"output.solutions[-1]",
709+
"output.solution",
710+
]
711+
712+
def make_path_exists(path):
713+
def path_exists(content_route, path):
714+
matches = jsonpath_ng.parse(path).find(content_route)
715+
return len(list(matches)) > 0
716+
717+
return lambda content_route, _: path_exists(content_route, path)
718+
719+
return MultiRoutePlotProfile(
720+
[
721+
(
722+
make_path_exists(p),
723+
RoutePlotProfile(
724+
jpath_route=f"{p}.vehicles[*].route",
725+
jpath_x="stop.location.lon",
726+
jpath_y="stop.location.lat",
727+
jpath_unassigned=f"{p}.unplanned[*]",
728+
jpath_unassigned_x="location.lon",
729+
jpath_unassigned_y="location.lat",
730+
),
731+
)
732+
for p in base_paths
733+
],
734+
"Input data does not match any known profile for nextroute plotting. Routes are expected at one of:\n"
735+
+ "\n".join(f"{p}.vehicles[*].route" for p in base_paths),
701736
)
702737

703738

0 commit comments

Comments
 (0)