blob: 16c792bcc7720a4ef436b5b7c3b96bc7cc3e73b8 [file] [log] [blame]
Ravago Jones6d460fe2021-07-03 16:59:55 -07001import gi
Ravago Jones5127ccc2022-07-31 16:32:45 -07002
Ravago Jones6d460fe2021-07-03 16:59:55 -07003gi.require_version('Gtk', '3.0')
4from gi.repository import Gtk
5import numpy as np
Ravago Jones56941a52021-07-31 14:42:38 -07006import queue
7import threading
8import copy
Ravago Jonesfa8da562022-07-02 18:10:22 -07009from multispline import Multispline
John Park91e69732019-03-03 13:12:43 -080010from libspline import Spline, DistanceSpline, Trajectory
11
Ravago Jones6d460fe2021-07-03 16:59:55 -070012from matplotlib.backends.backend_gtk3agg import (FigureCanvasGTK3Agg as
13 FigureCanvas)
14from matplotlib.figure import Figure
John Park91e69732019-03-03 13:12:43 -080015
Ravago Jones8da89c42022-07-17 19:34:06 -070016
Ravago Jones6d460fe2021-07-03 16:59:55 -070017class Graph(Gtk.Bin):
Ravago Jones5127ccc2022-07-31 16:32:45 -070018
Ravago Jones6d460fe2021-07-03 16:59:55 -070019 def __init__(self):
20 super(Graph, self).__init__()
21 fig = Figure(figsize=(5, 4), dpi=100)
22 self.axis = fig.add_subplot(111)
Ravago Jonese9584792022-05-28 16:16:46 -070023 self.canvas = FigureCanvas(fig) # a Gtk.DrawingArea
24 self.canvas.set_vexpand(True)
25 self.canvas.set_size_request(800, 250)
Ravago Jones318621a2023-04-09 18:23:12 -070026 self.mouse_move_callback = self.canvas.mpl_connect(
27 'motion_notify_event', self.on_mouse_move)
28 self.click_callback = self.canvas.mpl_connect('button_press_event',
29 self.on_click)
Ravago Jonese9584792022-05-28 16:16:46 -070030 self.add(self.canvas)
Ravago Jones56941a52021-07-31 14:42:38 -070031
Ravago Jones0a1d4092022-06-03 12:47:32 -070032 # The current graph data
33 self.data = None
34 # The size of a timestep
35 self.dt = 0.00505
36 # The position of the cursor in seconds
37 self.cursor = 0
38
39 # Reference to the parent gtk Widget that wants to get redrawn
40 # when user moves the cursor
41 self.cursor_watcher = None
42 self.cursor_line = None
43
44 self.queue = queue.Queue(maxsize=1)
Ravago Jones56941a52021-07-31 14:42:38 -070045 thread = threading.Thread(target=self.worker)
46 thread.daemon = True
47 thread.start()
48
Ravago Jones0a1d4092022-06-03 12:47:32 -070049 def find_cursor(self):
50 """Gets the cursor position as a distance along the spline"""
51 if self.data is None:
52 return None
53 cursor_index = int(self.cursor / self.dt)
Ryan Yinc11e1132022-08-24 21:02:10 -070054 if self.data[0].size < cursor_index:
Ravago Jonesac952dd2022-07-29 21:44:12 -070055 return None
Ravago Jones0a1d4092022-06-03 12:47:32 -070056 # use the time to index into the position data
Ryan Yinc11e1132022-08-24 21:02:10 -070057 try:
58 distance_at_cursor = self.data[0][cursor_index - 1]
59 multispline_index = int(self.data[5][cursor_index - 1])
60 return (multispline_index, distance_at_cursor)
61 except IndexError:
62 return None
Ravago Jones0a1d4092022-06-03 12:47:32 -070063
Ravago Jones291f5592022-07-07 20:40:37 -070064 def place_cursor(self, multispline_index, distance):
Ravago Jones0a1d4092022-06-03 12:47:32 -070065 """Places the cursor at a certain distance along the spline"""
66 if self.data is None:
67 return
Ravago Jones291f5592022-07-07 20:40:37 -070068
69 # find the section that is the current multispline
70 start_of_multispline = np.searchsorted(self.data[5],
71 multispline_index,
72 side='left')
73 end_of_multispline = np.searchsorted(self.data[5],
74 multispline_index,
75 side='right')
76 multispline_region = self.data[0][
77 start_of_multispline:end_of_multispline]
78
79 # convert distance along this multispline to time along trajectory
80 index = np.searchsorted(multispline_region, distance,
81 side='left') + start_of_multispline
Ravago Jones0a1d4092022-06-03 12:47:32 -070082 time = index * self.dt
Ravago Jones291f5592022-07-07 20:40:37 -070083
Ravago Jones0a1d4092022-06-03 12:47:32 -070084 self.cursor = time
85 self.redraw_cursor()
86
87 def on_mouse_move(self, event):
88 """Updates the cursor and all the canvases that watch it on mouse move"""
Ravago Jones291f5592022-07-07 20:40:37 -070089
Ravago Jones0a1d4092022-06-03 12:47:32 -070090 if self.data is None:
91 return
92 total_steps_taken = self.data.shape[1]
93 total_time = self.dt * total_steps_taken
94 if event.xdata is not None:
95 # clip the position if still on the canvas, but off the graph
96 self.cursor = np.clip(event.xdata, 0, total_time)
97
98 self.redraw_cursor()
99
100 # tell the field to update too
101 if self.cursor_watcher is not None:
102 self.cursor_watcher.queue_draw()
103
Ravago Jones318621a2023-04-09 18:23:12 -0700104 def on_click(self, event):
105 """Same as on_mouse_move but also selects multisplines"""
106
107 if self.data is None:
108 return
109 total_steps_taken = self.data.shape[1]
110 total_time = self.dt * total_steps_taken
111 if event.xdata is not None:
112 # clip the position if still on the canvas, but off the graph
113 self.cursor = np.clip(event.xdata, 0, total_time)
114
115 self.redraw_cursor()
116
117 # tell the field to update too
118 if self.cursor_watcher is not None:
119 self.cursor_watcher.queue_draw()
120 self.cursor_watcher.on_graph_clicked()
121
Ravago Jones0a1d4092022-06-03 12:47:32 -0700122 def redraw_cursor(self):
123 """Redraws the cursor line"""
124 # TODO: This redraws the entire graph and isn't very snappy
125 if self.cursor_line: self.cursor_line.remove()
126 self.cursor_line = self.axis.axvline(self.cursor)
127 self.canvas.draw_idle()
128
Ravago Jones291f5592022-07-07 20:40:37 -0700129 def schedule_recalculate(self, multisplines):
Ravago Jones0a1d4092022-06-03 12:47:32 -0700130 """Submits points to be graphed
131
132 Can be superseded by newer points if an old one isn't finished processing.
133 """
Ravago Jonesfa8da562022-07-02 18:10:22 -0700134
Ravago Jones291f5592022-07-07 20:40:37 -0700135 new_copy = copy.deepcopy(multisplines)
Ravago Jones56941a52021-07-31 14:42:38 -0700136
137 # empty the queue
138 try:
139 self.queue.get_nowait()
140 except queue.Empty:
Ravago Jones8da89c42022-07-17 19:34:06 -0700141 pass # was already empty
Ravago Jones56941a52021-07-31 14:42:38 -0700142
143 # replace with new request
144 self.queue.put_nowait(new_copy)
145
146 def worker(self):
147 while True:
148 self.recalculate_graph(self.queue.get())
John Park91e69732019-03-03 13:12:43 -0800149
Ravago Jones291f5592022-07-07 20:40:37 -0700150 def recalculate_graph(self, multisplines):
151 if len(multisplines) == 0: return
John Park91e69732019-03-03 13:12:43 -0800152
Ravago Jones6d460fe2021-07-03 16:59:55 -0700153 # call C++ wrappers to calculate the trajectory
Ravago Jones291f5592022-07-07 20:40:37 -0700154 full_data = None
155
156 for multispline_index, multispline in enumerate(multisplines):
157 multispline.update_lib_spline()
158 if len(multispline.getLibsplines()) == 0: continue
159 distanceSpline = DistanceSpline(multispline.getLibsplines())
160 traj = Trajectory(distanceSpline)
161 multispline.addConstraintsToTrajectory(traj)
162 traj.Plan()
163 XVA = traj.GetPlanXVA(self.dt)
164 if XVA is None: continue
165 position, _, _ = XVA
166
167 voltages = np.transpose([traj.Voltage(x) for x in position])
168
169 data = np.append(XVA, voltages, axis=0)
170
171 indicies = np.full((1, XVA.shape[1]), multispline_index, dtype=int)
172 data = np.append(data, indicies, axis=0)
173
174 if full_data is not None:
175 full_data = np.append(full_data, data, axis=1)
176 else:
177 full_data = data
178
179 if full_data is None: return
180 self.data = full_data
John Park91e69732019-03-03 13:12:43 -0800181
Ravago Jones6d460fe2021-07-03 16:59:55 -0700182 # extract values to be graphed
Ravago Jones291f5592022-07-07 20:40:37 -0700183 total_steps_taken = full_data.shape[1]
Ravago Jones0a1d4092022-06-03 12:47:32 -0700184 total_time = self.dt * total_steps_taken
185 times = np.linspace(0, total_time, num=total_steps_taken)
Ravago Jones291f5592022-07-07 20:40:37 -0700186 position, velocity, acceleration, left_voltage, right_voltage, _ = full_data
John Park91e69732019-03-03 13:12:43 -0800187
Ravago Jones6d460fe2021-07-03 16:59:55 -0700188 # update graph
189 self.axis.clear()
Ravago Jones0a1d4092022-06-03 12:47:32 -0700190 self.axis.plot(times, velocity)
191 self.axis.plot(times, acceleration)
192 self.axis.plot(times, left_voltage)
193 self.axis.plot(times, right_voltage)
Ravago Jones6d460fe2021-07-03 16:59:55 -0700194 self.axis.legend(
195 ["Velocity", "Acceleration", "Left Voltage", "Right Voltage"])
196 self.axis.xaxis.set_label_text("Time (sec)")
John Park91e69732019-03-03 13:12:43 -0800197
Ravago Jones6d460fe2021-07-03 16:59:55 -0700198 # renumber the x-axis to include the last point,
199 # the total time to drive the spline
200 self.axis.xaxis.set_ticks(np.linspace(0, total_time, num=8))
Ravago Jones128fb992021-07-31 13:56:58 -0700201
Ravago Jonese9584792022-05-28 16:16:46 -0700202 # redraw
Ravago Jones0a1d4092022-06-03 12:47:32 -0700203 self.canvas.draw_idle()