Ravago Jones | 6d460fe | 2021-07-03 16:59:55 -0700 | [diff] [blame] | 1 | import gi |
Ravago Jones | 5127ccc | 2022-07-31 16:32:45 -0700 | [diff] [blame] | 2 | |
Ravago Jones | 6d460fe | 2021-07-03 16:59:55 -0700 | [diff] [blame] | 3 | gi.require_version('Gtk', '3.0') |
| 4 | from gi.repository import Gtk |
| 5 | import numpy as np |
Ravago Jones | 56941a5 | 2021-07-31 14:42:38 -0700 | [diff] [blame] | 6 | import queue |
| 7 | import threading |
| 8 | import copy |
Ravago Jones | fa8da56 | 2022-07-02 18:10:22 -0700 | [diff] [blame] | 9 | from multispline import Multispline |
John Park | 91e6973 | 2019-03-03 13:12:43 -0800 | [diff] [blame] | 10 | from libspline import Spline, DistanceSpline, Trajectory |
| 11 | |
Ravago Jones | 6d460fe | 2021-07-03 16:59:55 -0700 | [diff] [blame] | 12 | from matplotlib.backends.backend_gtk3agg import (FigureCanvasGTK3Agg as |
| 13 | FigureCanvas) |
| 14 | from matplotlib.figure import Figure |
John Park | 91e6973 | 2019-03-03 13:12:43 -0800 | [diff] [blame] | 15 | |
Ravago Jones | 8da89c4 | 2022-07-17 19:34:06 -0700 | [diff] [blame] | 16 | |
Ravago Jones | 6d460fe | 2021-07-03 16:59:55 -0700 | [diff] [blame] | 17 | class Graph(Gtk.Bin): |
Ravago Jones | 5127ccc | 2022-07-31 16:32:45 -0700 | [diff] [blame] | 18 | |
Ravago Jones | 6d460fe | 2021-07-03 16:59:55 -0700 | [diff] [blame] | 19 | def __init__(self): |
| 20 | super(Graph, self).__init__() |
| 21 | fig = Figure(figsize=(5, 4), dpi=100) |
| 22 | self.axis = fig.add_subplot(111) |
Ravago Jones | e958479 | 2022-05-28 16:16:46 -0700 | [diff] [blame] | 23 | self.canvas = FigureCanvas(fig) # a Gtk.DrawingArea |
| 24 | self.canvas.set_vexpand(True) |
| 25 | self.canvas.set_size_request(800, 250) |
Ravago Jones | 318621a | 2023-04-09 18:23:12 -0700 | [diff] [blame] | 26 | 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 Jones | e958479 | 2022-05-28 16:16:46 -0700 | [diff] [blame] | 30 | self.add(self.canvas) |
Ravago Jones | 56941a5 | 2021-07-31 14:42:38 -0700 | [diff] [blame] | 31 | |
Ravago Jones | 0a1d409 | 2022-06-03 12:47:32 -0700 | [diff] [blame] | 32 | # 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 Jones | 56941a5 | 2021-07-31 14:42:38 -0700 | [diff] [blame] | 45 | thread = threading.Thread(target=self.worker) |
| 46 | thread.daemon = True |
| 47 | thread.start() |
| 48 | |
Ravago Jones | 0a1d409 | 2022-06-03 12:47:32 -0700 | [diff] [blame] | 49 | 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 Yin | c11e113 | 2022-08-24 21:02:10 -0700 | [diff] [blame] | 54 | if self.data[0].size < cursor_index: |
Ravago Jones | ac952dd | 2022-07-29 21:44:12 -0700 | [diff] [blame] | 55 | return None |
Ravago Jones | 0a1d409 | 2022-06-03 12:47:32 -0700 | [diff] [blame] | 56 | # use the time to index into the position data |
Ryan Yin | c11e113 | 2022-08-24 21:02:10 -0700 | [diff] [blame] | 57 | 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 Jones | 0a1d409 | 2022-06-03 12:47:32 -0700 | [diff] [blame] | 63 | |
Ravago Jones | 291f559 | 2022-07-07 20:40:37 -0700 | [diff] [blame] | 64 | def place_cursor(self, multispline_index, distance): |
Ravago Jones | 0a1d409 | 2022-06-03 12:47:32 -0700 | [diff] [blame] | 65 | """Places the cursor at a certain distance along the spline""" |
| 66 | if self.data is None: |
| 67 | return |
Ravago Jones | 291f559 | 2022-07-07 20:40:37 -0700 | [diff] [blame] | 68 | |
| 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 Jones | 0a1d409 | 2022-06-03 12:47:32 -0700 | [diff] [blame] | 82 | time = index * self.dt |
Ravago Jones | 291f559 | 2022-07-07 20:40:37 -0700 | [diff] [blame] | 83 | |
Ravago Jones | 0a1d409 | 2022-06-03 12:47:32 -0700 | [diff] [blame] | 84 | 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 Jones | 291f559 | 2022-07-07 20:40:37 -0700 | [diff] [blame] | 89 | |
Ravago Jones | 0a1d409 | 2022-06-03 12:47:32 -0700 | [diff] [blame] | 90 | 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 Jones | 318621a | 2023-04-09 18:23:12 -0700 | [diff] [blame] | 104 | 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 Jones | 0a1d409 | 2022-06-03 12:47:32 -0700 | [diff] [blame] | 122 | 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 Jones | 291f559 | 2022-07-07 20:40:37 -0700 | [diff] [blame] | 129 | def schedule_recalculate(self, multisplines): |
Ravago Jones | 0a1d409 | 2022-06-03 12:47:32 -0700 | [diff] [blame] | 130 | """Submits points to be graphed |
| 131 | |
| 132 | Can be superseded by newer points if an old one isn't finished processing. |
| 133 | """ |
Ravago Jones | fa8da56 | 2022-07-02 18:10:22 -0700 | [diff] [blame] | 134 | |
Ravago Jones | 291f559 | 2022-07-07 20:40:37 -0700 | [diff] [blame] | 135 | new_copy = copy.deepcopy(multisplines) |
Ravago Jones | 56941a5 | 2021-07-31 14:42:38 -0700 | [diff] [blame] | 136 | |
| 137 | # empty the queue |
| 138 | try: |
| 139 | self.queue.get_nowait() |
| 140 | except queue.Empty: |
Ravago Jones | 8da89c4 | 2022-07-17 19:34:06 -0700 | [diff] [blame] | 141 | pass # was already empty |
Ravago Jones | 56941a5 | 2021-07-31 14:42:38 -0700 | [diff] [blame] | 142 | |
| 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 Park | 91e6973 | 2019-03-03 13:12:43 -0800 | [diff] [blame] | 149 | |
Ravago Jones | 291f559 | 2022-07-07 20:40:37 -0700 | [diff] [blame] | 150 | def recalculate_graph(self, multisplines): |
| 151 | if len(multisplines) == 0: return |
John Park | 91e6973 | 2019-03-03 13:12:43 -0800 | [diff] [blame] | 152 | |
Ravago Jones | 6d460fe | 2021-07-03 16:59:55 -0700 | [diff] [blame] | 153 | # call C++ wrappers to calculate the trajectory |
Ravago Jones | 291f559 | 2022-07-07 20:40:37 -0700 | [diff] [blame] | 154 | 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 Park | 91e6973 | 2019-03-03 13:12:43 -0800 | [diff] [blame] | 181 | |
Ravago Jones | 6d460fe | 2021-07-03 16:59:55 -0700 | [diff] [blame] | 182 | # extract values to be graphed |
Ravago Jones | 291f559 | 2022-07-07 20:40:37 -0700 | [diff] [blame] | 183 | total_steps_taken = full_data.shape[1] |
Ravago Jones | 0a1d409 | 2022-06-03 12:47:32 -0700 | [diff] [blame] | 184 | total_time = self.dt * total_steps_taken |
| 185 | times = np.linspace(0, total_time, num=total_steps_taken) |
Ravago Jones | 291f559 | 2022-07-07 20:40:37 -0700 | [diff] [blame] | 186 | position, velocity, acceleration, left_voltage, right_voltage, _ = full_data |
John Park | 91e6973 | 2019-03-03 13:12:43 -0800 | [diff] [blame] | 187 | |
Ravago Jones | 6d460fe | 2021-07-03 16:59:55 -0700 | [diff] [blame] | 188 | # update graph |
| 189 | self.axis.clear() |
Ravago Jones | 0a1d409 | 2022-06-03 12:47:32 -0700 | [diff] [blame] | 190 | 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 Jones | 6d460fe | 2021-07-03 16:59:55 -0700 | [diff] [blame] | 194 | self.axis.legend( |
| 195 | ["Velocity", "Acceleration", "Left Voltage", "Right Voltage"]) |
| 196 | self.axis.xaxis.set_label_text("Time (sec)") |
John Park | 91e6973 | 2019-03-03 13:12:43 -0800 | [diff] [blame] | 197 | |
Ravago Jones | 6d460fe | 2021-07-03 16:59:55 -0700 | [diff] [blame] | 198 | # 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 Jones | 128fb99 | 2021-07-31 13:56:58 -0700 | [diff] [blame] | 201 | |
Ravago Jones | e958479 | 2022-05-28 16:16:46 -0700 | [diff] [blame] | 202 | # redraw |
Ravago Jones | 0a1d409 | 2022-06-03 12:47:32 -0700 | [diff] [blame] | 203 | self.canvas.draw_idle() |