Ravago Jones | 6d460fe | 2021-07-03 16:59:55 -0700 | [diff] [blame] | 1 | import gi |
| 2 | gi.require_version('Gtk', '3.0') |
| 3 | from gi.repository import Gtk |
| 4 | import numpy as np |
Ravago Jones | 56941a5 | 2021-07-31 14:42:38 -0700 | [diff] [blame] | 5 | import queue |
| 6 | import threading |
| 7 | import copy |
Ravago Jones | fa8da56 | 2022-07-02 18:10:22 -0700 | [diff] [blame] | 8 | from multispline import Multispline |
John Park | 91e6973 | 2019-03-03 13:12:43 -0800 | [diff] [blame] | 9 | from libspline import Spline, DistanceSpline, Trajectory |
| 10 | |
Ravago Jones | 6d460fe | 2021-07-03 16:59:55 -0700 | [diff] [blame] | 11 | from matplotlib.backends.backend_gtk3agg import (FigureCanvasGTK3Agg as |
| 12 | FigureCanvas) |
| 13 | from matplotlib.figure import Figure |
John Park | 91e6973 | 2019-03-03 13:12:43 -0800 | [diff] [blame] | 14 | |
Ravago Jones | 8da89c4 | 2022-07-17 19:34:06 -0700 | [diff] [blame] | 15 | |
Ravago Jones | 6d460fe | 2021-07-03 16:59:55 -0700 | [diff] [blame] | 16 | class Graph(Gtk.Bin): |
| 17 | def __init__(self): |
| 18 | super(Graph, self).__init__() |
| 19 | fig = Figure(figsize=(5, 4), dpi=100) |
| 20 | self.axis = fig.add_subplot(111) |
Ravago Jones | e958479 | 2022-05-28 16:16:46 -0700 | [diff] [blame] | 21 | self.canvas = FigureCanvas(fig) # a Gtk.DrawingArea |
| 22 | self.canvas.set_vexpand(True) |
| 23 | self.canvas.set_size_request(800, 250) |
Ravago Jones | 0a1d409 | 2022-06-03 12:47:32 -0700 | [diff] [blame] | 24 | self.callback_id = self.canvas.mpl_connect('motion_notify_event', |
| 25 | self.on_mouse_move) |
Ravago Jones | e958479 | 2022-05-28 16:16:46 -0700 | [diff] [blame] | 26 | self.add(self.canvas) |
Ravago Jones | 56941a5 | 2021-07-31 14:42:38 -0700 | [diff] [blame] | 27 | |
Ravago Jones | 0a1d409 | 2022-06-03 12:47:32 -0700 | [diff] [blame] | 28 | # The current graph data |
| 29 | self.data = None |
| 30 | # The size of a timestep |
| 31 | self.dt = 0.00505 |
| 32 | # The position of the cursor in seconds |
| 33 | self.cursor = 0 |
| 34 | |
| 35 | # Reference to the parent gtk Widget that wants to get redrawn |
| 36 | # when user moves the cursor |
| 37 | self.cursor_watcher = None |
| 38 | self.cursor_line = None |
| 39 | |
| 40 | self.queue = queue.Queue(maxsize=1) |
Ravago Jones | 56941a5 | 2021-07-31 14:42:38 -0700 | [diff] [blame] | 41 | thread = threading.Thread(target=self.worker) |
| 42 | thread.daemon = True |
| 43 | thread.start() |
| 44 | |
Ravago Jones | 0a1d409 | 2022-06-03 12:47:32 -0700 | [diff] [blame] | 45 | def find_cursor(self): |
| 46 | """Gets the cursor position as a distance along the spline""" |
| 47 | if self.data is None: |
| 48 | return None |
| 49 | cursor_index = int(self.cursor / self.dt) |
Ravago Jones | ac952dd | 2022-07-29 21:44:12 -0700 | [diff] [blame^] | 50 | if cursor_index > self.data.size: |
| 51 | return None |
Ravago Jones | 0a1d409 | 2022-06-03 12:47:32 -0700 | [diff] [blame] | 52 | # use the time to index into the position data |
| 53 | distance_at_cursor = self.data[0][cursor_index - 1] |
Ravago Jones | 291f559 | 2022-07-07 20:40:37 -0700 | [diff] [blame] | 54 | multispline_index = int(self.data[5][cursor_index - 1]) |
| 55 | return (multispline_index, distance_at_cursor) |
Ravago Jones | 0a1d409 | 2022-06-03 12:47:32 -0700 | [diff] [blame] | 56 | |
Ravago Jones | 291f559 | 2022-07-07 20:40:37 -0700 | [diff] [blame] | 57 | def place_cursor(self, multispline_index, distance): |
Ravago Jones | 0a1d409 | 2022-06-03 12:47:32 -0700 | [diff] [blame] | 58 | """Places the cursor at a certain distance along the spline""" |
| 59 | if self.data is None: |
| 60 | return |
Ravago Jones | 291f559 | 2022-07-07 20:40:37 -0700 | [diff] [blame] | 61 | |
| 62 | # find the section that is the current multispline |
| 63 | start_of_multispline = np.searchsorted(self.data[5], |
| 64 | multispline_index, |
| 65 | side='left') |
| 66 | end_of_multispline = np.searchsorted(self.data[5], |
| 67 | multispline_index, |
| 68 | side='right') |
| 69 | multispline_region = self.data[0][ |
| 70 | start_of_multispline:end_of_multispline] |
| 71 | |
| 72 | # convert distance along this multispline to time along trajectory |
| 73 | index = np.searchsorted(multispline_region, distance, |
| 74 | side='left') + start_of_multispline |
Ravago Jones | 0a1d409 | 2022-06-03 12:47:32 -0700 | [diff] [blame] | 75 | time = index * self.dt |
Ravago Jones | 291f559 | 2022-07-07 20:40:37 -0700 | [diff] [blame] | 76 | |
Ravago Jones | 0a1d409 | 2022-06-03 12:47:32 -0700 | [diff] [blame] | 77 | self.cursor = time |
| 78 | self.redraw_cursor() |
| 79 | |
| 80 | def on_mouse_move(self, event): |
| 81 | """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] | 82 | |
Ravago Jones | 0a1d409 | 2022-06-03 12:47:32 -0700 | [diff] [blame] | 83 | if self.data is None: |
| 84 | return |
| 85 | total_steps_taken = self.data.shape[1] |
| 86 | total_time = self.dt * total_steps_taken |
| 87 | if event.xdata is not None: |
| 88 | # clip the position if still on the canvas, but off the graph |
| 89 | self.cursor = np.clip(event.xdata, 0, total_time) |
| 90 | |
| 91 | self.redraw_cursor() |
| 92 | |
| 93 | # tell the field to update too |
| 94 | if self.cursor_watcher is not None: |
| 95 | self.cursor_watcher.queue_draw() |
| 96 | |
| 97 | def redraw_cursor(self): |
| 98 | """Redraws the cursor line""" |
| 99 | # TODO: This redraws the entire graph and isn't very snappy |
| 100 | if self.cursor_line: self.cursor_line.remove() |
| 101 | self.cursor_line = self.axis.axvline(self.cursor) |
| 102 | self.canvas.draw_idle() |
| 103 | |
Ravago Jones | 291f559 | 2022-07-07 20:40:37 -0700 | [diff] [blame] | 104 | def schedule_recalculate(self, multisplines): |
Ravago Jones | 0a1d409 | 2022-06-03 12:47:32 -0700 | [diff] [blame] | 105 | """Submits points to be graphed |
| 106 | |
| 107 | Can be superseded by newer points if an old one isn't finished processing. |
| 108 | """ |
Ravago Jones | fa8da56 | 2022-07-02 18:10:22 -0700 | [diff] [blame] | 109 | |
Ravago Jones | 291f559 | 2022-07-07 20:40:37 -0700 | [diff] [blame] | 110 | new_copy = copy.deepcopy(multisplines) |
Ravago Jones | 56941a5 | 2021-07-31 14:42:38 -0700 | [diff] [blame] | 111 | |
| 112 | # empty the queue |
| 113 | try: |
| 114 | self.queue.get_nowait() |
| 115 | except queue.Empty: |
Ravago Jones | 8da89c4 | 2022-07-17 19:34:06 -0700 | [diff] [blame] | 116 | pass # was already empty |
Ravago Jones | 56941a5 | 2021-07-31 14:42:38 -0700 | [diff] [blame] | 117 | |
| 118 | # replace with new request |
| 119 | self.queue.put_nowait(new_copy) |
| 120 | |
| 121 | def worker(self): |
| 122 | while True: |
| 123 | self.recalculate_graph(self.queue.get()) |
John Park | 91e6973 | 2019-03-03 13:12:43 -0800 | [diff] [blame] | 124 | |
Ravago Jones | 291f559 | 2022-07-07 20:40:37 -0700 | [diff] [blame] | 125 | def recalculate_graph(self, multisplines): |
| 126 | if len(multisplines) == 0: return |
John Park | 91e6973 | 2019-03-03 13:12:43 -0800 | [diff] [blame] | 127 | |
Ravago Jones | 6d460fe | 2021-07-03 16:59:55 -0700 | [diff] [blame] | 128 | # call C++ wrappers to calculate the trajectory |
Ravago Jones | 291f559 | 2022-07-07 20:40:37 -0700 | [diff] [blame] | 129 | full_data = None |
| 130 | |
| 131 | for multispline_index, multispline in enumerate(multisplines): |
| 132 | multispline.update_lib_spline() |
| 133 | if len(multispline.getLibsplines()) == 0: continue |
| 134 | distanceSpline = DistanceSpline(multispline.getLibsplines()) |
| 135 | traj = Trajectory(distanceSpline) |
| 136 | multispline.addConstraintsToTrajectory(traj) |
| 137 | traj.Plan() |
| 138 | XVA = traj.GetPlanXVA(self.dt) |
| 139 | if XVA is None: continue |
| 140 | position, _, _ = XVA |
| 141 | |
| 142 | voltages = np.transpose([traj.Voltage(x) for x in position]) |
| 143 | |
| 144 | data = np.append(XVA, voltages, axis=0) |
| 145 | |
| 146 | indicies = np.full((1, XVA.shape[1]), multispline_index, dtype=int) |
| 147 | data = np.append(data, indicies, axis=0) |
| 148 | |
| 149 | if full_data is not None: |
| 150 | full_data = np.append(full_data, data, axis=1) |
| 151 | else: |
| 152 | full_data = data |
| 153 | |
| 154 | if full_data is None: return |
| 155 | self.data = full_data |
John Park | 91e6973 | 2019-03-03 13:12:43 -0800 | [diff] [blame] | 156 | |
Ravago Jones | 6d460fe | 2021-07-03 16:59:55 -0700 | [diff] [blame] | 157 | # extract values to be graphed |
Ravago Jones | 291f559 | 2022-07-07 20:40:37 -0700 | [diff] [blame] | 158 | total_steps_taken = full_data.shape[1] |
Ravago Jones | 0a1d409 | 2022-06-03 12:47:32 -0700 | [diff] [blame] | 159 | total_time = self.dt * total_steps_taken |
| 160 | times = np.linspace(0, total_time, num=total_steps_taken) |
Ravago Jones | 291f559 | 2022-07-07 20:40:37 -0700 | [diff] [blame] | 161 | position, velocity, acceleration, left_voltage, right_voltage, _ = full_data |
John Park | 91e6973 | 2019-03-03 13:12:43 -0800 | [diff] [blame] | 162 | |
Ravago Jones | 6d460fe | 2021-07-03 16:59:55 -0700 | [diff] [blame] | 163 | # update graph |
| 164 | self.axis.clear() |
Ravago Jones | 0a1d409 | 2022-06-03 12:47:32 -0700 | [diff] [blame] | 165 | self.axis.plot(times, velocity) |
| 166 | self.axis.plot(times, acceleration) |
| 167 | self.axis.plot(times, left_voltage) |
| 168 | self.axis.plot(times, right_voltage) |
Ravago Jones | 6d460fe | 2021-07-03 16:59:55 -0700 | [diff] [blame] | 169 | self.axis.legend( |
| 170 | ["Velocity", "Acceleration", "Left Voltage", "Right Voltage"]) |
| 171 | self.axis.xaxis.set_label_text("Time (sec)") |
John Park | 91e6973 | 2019-03-03 13:12:43 -0800 | [diff] [blame] | 172 | |
Ravago Jones | 6d460fe | 2021-07-03 16:59:55 -0700 | [diff] [blame] | 173 | # renumber the x-axis to include the last point, |
| 174 | # the total time to drive the spline |
| 175 | self.axis.xaxis.set_ticks(np.linspace(0, total_time, num=8)) |
Ravago Jones | 128fb99 | 2021-07-31 13:56:58 -0700 | [diff] [blame] | 176 | |
Ravago Jones | e958479 | 2022-05-28 16:16:46 -0700 | [diff] [blame] | 177 | # redraw |
Ravago Jones | 0a1d409 | 2022-06-03 12:47:32 -0700 | [diff] [blame] | 178 | self.canvas.draw_idle() |