blob: 1cc6f57a51a5e8a9ddb0a66cef2917d453826ccb [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 Jones0a1d4092022-06-03 12:47:32 -070026 self.callback_id = self.canvas.mpl_connect('motion_notify_event',
27 self.on_mouse_move)
Ravago Jonese9584792022-05-28 16:16:46 -070028 self.add(self.canvas)
Ravago Jones56941a52021-07-31 14:42:38 -070029
Ravago Jones0a1d4092022-06-03 12:47:32 -070030 # The current graph data
31 self.data = None
32 # The size of a timestep
33 self.dt = 0.00505
34 # The position of the cursor in seconds
35 self.cursor = 0
36
37 # Reference to the parent gtk Widget that wants to get redrawn
38 # when user moves the cursor
39 self.cursor_watcher = None
40 self.cursor_line = None
41
42 self.queue = queue.Queue(maxsize=1)
Ravago Jones56941a52021-07-31 14:42:38 -070043 thread = threading.Thread(target=self.worker)
44 thread.daemon = True
45 thread.start()
46
Ravago Jones0a1d4092022-06-03 12:47:32 -070047 def find_cursor(self):
48 """Gets the cursor position as a distance along the spline"""
49 if self.data is None:
50 return None
51 cursor_index = int(self.cursor / self.dt)
Ryan Yinc11e1132022-08-24 21:02:10 -070052 if self.data[0].size < cursor_index:
Ravago Jonesac952dd2022-07-29 21:44:12 -070053 return None
Ravago Jones0a1d4092022-06-03 12:47:32 -070054 # use the time to index into the position data
Ryan Yinc11e1132022-08-24 21:02:10 -070055 try:
56 distance_at_cursor = self.data[0][cursor_index - 1]
57 multispline_index = int(self.data[5][cursor_index - 1])
58 return (multispline_index, distance_at_cursor)
59 except IndexError:
60 return None
Ravago Jones0a1d4092022-06-03 12:47:32 -070061
Ravago Jones291f5592022-07-07 20:40:37 -070062 def place_cursor(self, multispline_index, distance):
Ravago Jones0a1d4092022-06-03 12:47:32 -070063 """Places the cursor at a certain distance along the spline"""
64 if self.data is None:
65 return
Ravago Jones291f5592022-07-07 20:40:37 -070066
67 # find the section that is the current multispline
68 start_of_multispline = np.searchsorted(self.data[5],
69 multispline_index,
70 side='left')
71 end_of_multispline = np.searchsorted(self.data[5],
72 multispline_index,
73 side='right')
74 multispline_region = self.data[0][
75 start_of_multispline:end_of_multispline]
76
77 # convert distance along this multispline to time along trajectory
78 index = np.searchsorted(multispline_region, distance,
79 side='left') + start_of_multispline
Ravago Jones0a1d4092022-06-03 12:47:32 -070080 time = index * self.dt
Ravago Jones291f5592022-07-07 20:40:37 -070081
Ravago Jones0a1d4092022-06-03 12:47:32 -070082 self.cursor = time
83 self.redraw_cursor()
84
85 def on_mouse_move(self, event):
86 """Updates the cursor and all the canvases that watch it on mouse move"""
Ravago Jones291f5592022-07-07 20:40:37 -070087
Ravago Jones0a1d4092022-06-03 12:47:32 -070088 if self.data is None:
89 return
90 total_steps_taken = self.data.shape[1]
91 total_time = self.dt * total_steps_taken
92 if event.xdata is not None:
93 # clip the position if still on the canvas, but off the graph
94 self.cursor = np.clip(event.xdata, 0, total_time)
95
96 self.redraw_cursor()
97
98 # tell the field to update too
99 if self.cursor_watcher is not None:
100 self.cursor_watcher.queue_draw()
101
102 def redraw_cursor(self):
103 """Redraws the cursor line"""
104 # TODO: This redraws the entire graph and isn't very snappy
105 if self.cursor_line: self.cursor_line.remove()
106 self.cursor_line = self.axis.axvline(self.cursor)
107 self.canvas.draw_idle()
108
Ravago Jones291f5592022-07-07 20:40:37 -0700109 def schedule_recalculate(self, multisplines):
Ravago Jones0a1d4092022-06-03 12:47:32 -0700110 """Submits points to be graphed
111
112 Can be superseded by newer points if an old one isn't finished processing.
113 """
Ravago Jonesfa8da562022-07-02 18:10:22 -0700114
Ravago Jones291f5592022-07-07 20:40:37 -0700115 new_copy = copy.deepcopy(multisplines)
Ravago Jones56941a52021-07-31 14:42:38 -0700116
117 # empty the queue
118 try:
119 self.queue.get_nowait()
120 except queue.Empty:
Ravago Jones8da89c42022-07-17 19:34:06 -0700121 pass # was already empty
Ravago Jones56941a52021-07-31 14:42:38 -0700122
123 # replace with new request
124 self.queue.put_nowait(new_copy)
125
126 def worker(self):
127 while True:
128 self.recalculate_graph(self.queue.get())
John Park91e69732019-03-03 13:12:43 -0800129
Ravago Jones291f5592022-07-07 20:40:37 -0700130 def recalculate_graph(self, multisplines):
131 if len(multisplines) == 0: return
John Park91e69732019-03-03 13:12:43 -0800132
Ravago Jones6d460fe2021-07-03 16:59:55 -0700133 # call C++ wrappers to calculate the trajectory
Ravago Jones291f5592022-07-07 20:40:37 -0700134 full_data = None
135
136 for multispline_index, multispline in enumerate(multisplines):
137 multispline.update_lib_spline()
138 if len(multispline.getLibsplines()) == 0: continue
139 distanceSpline = DistanceSpline(multispline.getLibsplines())
140 traj = Trajectory(distanceSpline)
141 multispline.addConstraintsToTrajectory(traj)
142 traj.Plan()
143 XVA = traj.GetPlanXVA(self.dt)
144 if XVA is None: continue
145 position, _, _ = XVA
146
147 voltages = np.transpose([traj.Voltage(x) for x in position])
148
149 data = np.append(XVA, voltages, axis=0)
150
151 indicies = np.full((1, XVA.shape[1]), multispline_index, dtype=int)
152 data = np.append(data, indicies, axis=0)
153
154 if full_data is not None:
155 full_data = np.append(full_data, data, axis=1)
156 else:
157 full_data = data
158
159 if full_data is None: return
160 self.data = full_data
John Park91e69732019-03-03 13:12:43 -0800161
Ravago Jones6d460fe2021-07-03 16:59:55 -0700162 # extract values to be graphed
Ravago Jones291f5592022-07-07 20:40:37 -0700163 total_steps_taken = full_data.shape[1]
Ravago Jones0a1d4092022-06-03 12:47:32 -0700164 total_time = self.dt * total_steps_taken
165 times = np.linspace(0, total_time, num=total_steps_taken)
Ravago Jones291f5592022-07-07 20:40:37 -0700166 position, velocity, acceleration, left_voltage, right_voltage, _ = full_data
John Park91e69732019-03-03 13:12:43 -0800167
Ravago Jones6d460fe2021-07-03 16:59:55 -0700168 # update graph
169 self.axis.clear()
Ravago Jones0a1d4092022-06-03 12:47:32 -0700170 self.axis.plot(times, velocity)
171 self.axis.plot(times, acceleration)
172 self.axis.plot(times, left_voltage)
173 self.axis.plot(times, right_voltage)
Ravago Jones6d460fe2021-07-03 16:59:55 -0700174 self.axis.legend(
175 ["Velocity", "Acceleration", "Left Voltage", "Right Voltage"])
176 self.axis.xaxis.set_label_text("Time (sec)")
John Park91e69732019-03-03 13:12:43 -0800177
Ravago Jones6d460fe2021-07-03 16:59:55 -0700178 # renumber the x-axis to include the last point,
179 # the total time to drive the spline
180 self.axis.xaxis.set_ticks(np.linspace(0, total_time, num=8))
Ravago Jones128fb992021-07-31 13:56:58 -0700181
Ravago Jonese9584792022-05-28 16:16:46 -0700182 # redraw
Ravago Jones0a1d4092022-06-03 12:47:32 -0700183 self.canvas.draw_idle()