blob: 178b63d429a4be10fca7659c8a2f4e01089b9d25 [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)
Ravago Jonesac952dd2022-07-29 21:44:12 -070052 if cursor_index > self.data.size:
53 return None
Ravago Jones0a1d4092022-06-03 12:47:32 -070054 # use the time to index into the position data
55 distance_at_cursor = self.data[0][cursor_index - 1]
Ravago Jones291f5592022-07-07 20:40:37 -070056 multispline_index = int(self.data[5][cursor_index - 1])
57 return (multispline_index, distance_at_cursor)
Ravago Jones0a1d4092022-06-03 12:47:32 -070058
Ravago Jones291f5592022-07-07 20:40:37 -070059 def place_cursor(self, multispline_index, distance):
Ravago Jones0a1d4092022-06-03 12:47:32 -070060 """Places the cursor at a certain distance along the spline"""
61 if self.data is None:
62 return
Ravago Jones291f5592022-07-07 20:40:37 -070063
64 # find the section that is the current multispline
65 start_of_multispline = np.searchsorted(self.data[5],
66 multispline_index,
67 side='left')
68 end_of_multispline = np.searchsorted(self.data[5],
69 multispline_index,
70 side='right')
71 multispline_region = self.data[0][
72 start_of_multispline:end_of_multispline]
73
74 # convert distance along this multispline to time along trajectory
75 index = np.searchsorted(multispline_region, distance,
76 side='left') + start_of_multispline
Ravago Jones0a1d4092022-06-03 12:47:32 -070077 time = index * self.dt
Ravago Jones291f5592022-07-07 20:40:37 -070078
Ravago Jones0a1d4092022-06-03 12:47:32 -070079 self.cursor = time
80 self.redraw_cursor()
81
82 def on_mouse_move(self, event):
83 """Updates the cursor and all the canvases that watch it on mouse move"""
Ravago Jones291f5592022-07-07 20:40:37 -070084
Ravago Jones0a1d4092022-06-03 12:47:32 -070085 if self.data is None:
86 return
87 total_steps_taken = self.data.shape[1]
88 total_time = self.dt * total_steps_taken
89 if event.xdata is not None:
90 # clip the position if still on the canvas, but off the graph
91 self.cursor = np.clip(event.xdata, 0, total_time)
92
93 self.redraw_cursor()
94
95 # tell the field to update too
96 if self.cursor_watcher is not None:
97 self.cursor_watcher.queue_draw()
98
99 def redraw_cursor(self):
100 """Redraws the cursor line"""
101 # TODO: This redraws the entire graph and isn't very snappy
102 if self.cursor_line: self.cursor_line.remove()
103 self.cursor_line = self.axis.axvline(self.cursor)
104 self.canvas.draw_idle()
105
Ravago Jones291f5592022-07-07 20:40:37 -0700106 def schedule_recalculate(self, multisplines):
Ravago Jones0a1d4092022-06-03 12:47:32 -0700107 """Submits points to be graphed
108
109 Can be superseded by newer points if an old one isn't finished processing.
110 """
Ravago Jonesfa8da562022-07-02 18:10:22 -0700111
Ravago Jones291f5592022-07-07 20:40:37 -0700112 new_copy = copy.deepcopy(multisplines)
Ravago Jones56941a52021-07-31 14:42:38 -0700113
114 # empty the queue
115 try:
116 self.queue.get_nowait()
117 except queue.Empty:
Ravago Jones8da89c42022-07-17 19:34:06 -0700118 pass # was already empty
Ravago Jones56941a52021-07-31 14:42:38 -0700119
120 # replace with new request
121 self.queue.put_nowait(new_copy)
122
123 def worker(self):
124 while True:
125 self.recalculate_graph(self.queue.get())
John Park91e69732019-03-03 13:12:43 -0800126
Ravago Jones291f5592022-07-07 20:40:37 -0700127 def recalculate_graph(self, multisplines):
128 if len(multisplines) == 0: return
John Park91e69732019-03-03 13:12:43 -0800129
Ravago Jones6d460fe2021-07-03 16:59:55 -0700130 # call C++ wrappers to calculate the trajectory
Ravago Jones291f5592022-07-07 20:40:37 -0700131 full_data = None
132
133 for multispline_index, multispline in enumerate(multisplines):
134 multispline.update_lib_spline()
135 if len(multispline.getLibsplines()) == 0: continue
136 distanceSpline = DistanceSpline(multispline.getLibsplines())
137 traj = Trajectory(distanceSpline)
138 multispline.addConstraintsToTrajectory(traj)
139 traj.Plan()
140 XVA = traj.GetPlanXVA(self.dt)
141 if XVA is None: continue
142 position, _, _ = XVA
143
144 voltages = np.transpose([traj.Voltage(x) for x in position])
145
146 data = np.append(XVA, voltages, axis=0)
147
148 indicies = np.full((1, XVA.shape[1]), multispline_index, dtype=int)
149 data = np.append(data, indicies, axis=0)
150
151 if full_data is not None:
152 full_data = np.append(full_data, data, axis=1)
153 else:
154 full_data = data
155
156 if full_data is None: return
157 self.data = full_data
John Park91e69732019-03-03 13:12:43 -0800158
Ravago Jones6d460fe2021-07-03 16:59:55 -0700159 # extract values to be graphed
Ravago Jones291f5592022-07-07 20:40:37 -0700160 total_steps_taken = full_data.shape[1]
Ravago Jones0a1d4092022-06-03 12:47:32 -0700161 total_time = self.dt * total_steps_taken
162 times = np.linspace(0, total_time, num=total_steps_taken)
Ravago Jones291f5592022-07-07 20:40:37 -0700163 position, velocity, acceleration, left_voltage, right_voltage, _ = full_data
John Park91e69732019-03-03 13:12:43 -0800164
Ravago Jones6d460fe2021-07-03 16:59:55 -0700165 # update graph
166 self.axis.clear()
Ravago Jones0a1d4092022-06-03 12:47:32 -0700167 self.axis.plot(times, velocity)
168 self.axis.plot(times, acceleration)
169 self.axis.plot(times, left_voltage)
170 self.axis.plot(times, right_voltage)
Ravago Jones6d460fe2021-07-03 16:59:55 -0700171 self.axis.legend(
172 ["Velocity", "Acceleration", "Left Voltage", "Right Voltage"])
173 self.axis.xaxis.set_label_text("Time (sec)")
John Park91e69732019-03-03 13:12:43 -0800174
Ravago Jones6d460fe2021-07-03 16:59:55 -0700175 # renumber the x-axis to include the last point,
176 # the total time to drive the spline
177 self.axis.xaxis.set_ticks(np.linspace(0, total_time, num=8))
Ravago Jones128fb992021-07-31 13:56:58 -0700178
Ravago Jonese9584792022-05-28 16:16:46 -0700179 # redraw
Ravago Jones0a1d4092022-06-03 12:47:32 -0700180 self.canvas.draw_idle()