PlotCanvas.py 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499
  1. ############################################################
  2. # FlatCAM: 2D Post-processing for Manufacturing #
  3. # http://caram.cl/software/flatcam #
  4. # Author: Juan Pablo Caram (c) #
  5. # Date: 2/5/2014 #
  6. # MIT Licence #
  7. ############################################################
  8. from PyQt4 import QtGui, QtCore
  9. # Prevent conflict with Qt5 and above.
  10. from matplotlib import use as mpl_use
  11. mpl_use("Qt4Agg")
  12. from matplotlib.figure import Figure
  13. from matplotlib.backends.backend_qt4agg import FigureCanvasQTAgg as FigureCanvas
  14. from matplotlib.backends.backend_agg import FigureCanvasAgg
  15. import FlatCAMApp
  16. import logging
  17. log = logging.getLogger('base')
  18. class CanvasCache(QtCore.QObject):
  19. # Signals:
  20. # A bitmap is ready to be displayed.
  21. new_screen = QtCore.pyqtSignal()
  22. def __init__(self, plotcanvas, dpi=50):
  23. super(CanvasCache, self).__init__()
  24. self.plotcanvas = plotcanvas
  25. self.dpi = dpi
  26. self.figure = Figure(dpi=dpi)
  27. self.axes = self.figure.add_axes([0.0, 0.0, 1.0, 1.0], alpha=1.0)
  28. self.axes.set_frame_on(False)
  29. self.axes.set_xticks([])
  30. self.axes.set_yticks([])
  31. self.canvas = FigureCanvasAgg(self.figure)
  32. self.cache = None
  33. def run(self):
  34. log.debug("CanvasCache Thread Started!")
  35. self.plotcanvas.update_screen_request.connect(self.on_update_req)
  36. def on_update_req(self, extents):
  37. """
  38. Event handler for an updated display request.
  39. :param extents: [xmin, xmax, ymin, ymax, zoom(optional)]
  40. """
  41. log.debug("Canvas update requested: %s" % str(extents))
  42. # Note: This information here might be out of date. Establish
  43. # a protocol regarding when to change the canvas in the main
  44. # thread and when to check these values here in the background,
  45. # or pass this data in the signal (safer).
  46. log.debug("Size: %s [px]" % str(self.plotcanvas.get_axes_pixelsize()))
  47. log.debug("Density: %s [units/px]" % str(self.plotcanvas.get_density()))
  48. # Move the requested screen portion to the main thread
  49. # and inform about the update:
  50. self.new_screen.emit()
  51. # Continue to update the cache.
  52. class PlotCanvas(QtCore.QObject):
  53. """
  54. Class handling the plotting area in the application.
  55. """
  56. # Signals:
  57. # Request for new bitmap to display. The parameter
  58. # is a list with [xmin, xmax, ymin, ymax, zoom(optional)]
  59. update_screen_request = QtCore.pyqtSignal(list)
  60. def __init__(self, container):
  61. """
  62. The constructor configures the Matplotlib figure that
  63. will contain all plots, creates the base axes and connects
  64. events to the plotting area.
  65. :param container: The parent container in which to draw plots.
  66. :rtype: PlotCanvas
  67. """
  68. super(PlotCanvas, self).__init__()
  69. # Options
  70. self.x_margin = 15 # pixels
  71. self.y_margin = 25 # Pixels
  72. # Parent container
  73. self.container = container
  74. # Plots go onto a single matplotlib.figure
  75. self.figure = Figure(dpi=50) # TODO: dpi needed?
  76. self.figure.patch.set_visible(False)
  77. # These axes show the ticks and grid. No plotting done here.
  78. # New axes must have a label, otherwise mpl returns an existing one.
  79. self.axes = self.figure.add_axes([0.05, 0.05, 0.9, 0.9], label="base", alpha=0.0)
  80. self.axes.set_aspect(1)
  81. self.axes.grid(True)
  82. # The canvas is the top level container (FigureCanvasQTAgg)
  83. self.canvas = FigureCanvas(self.figure)
  84. # self.canvas.setFocusPolicy(QtCore.Qt.ClickFocus)
  85. # self.canvas.setFocus()
  86. #self.canvas.set_hexpand(1)
  87. #self.canvas.set_vexpand(1)
  88. #self.canvas.set_can_focus(True) # For key press
  89. # Attach to parent
  90. #self.container.attach(self.canvas, 0, 0, 600, 400) # TODO: Height and width are num. columns??
  91. self.container.addWidget(self.canvas) # Qt
  92. # Copy a bitmap of the canvas for quick animation.
  93. # Update every time the canvas is re-drawn.
  94. self.background = self.canvas.copy_from_bbox(self.axes.bbox)
  95. ### Bitmap Cache
  96. self.cache = CanvasCache(self)
  97. self.cache_thread = QtCore.QThread()
  98. self.cache.moveToThread(self.cache_thread)
  99. super(PlotCanvas, self).connect(self.cache_thread, QtCore.SIGNAL("started()"), self.cache.run)
  100. # self.connect()
  101. self.cache_thread.start()
  102. self.cache.new_screen.connect(self.on_new_screen)
  103. # Events
  104. self.canvas.mpl_connect('button_press_event', self.on_mouse_press)
  105. self.canvas.mpl_connect('button_release_event', self.on_mouse_release)
  106. self.canvas.mpl_connect('motion_notify_event', self.on_mouse_move)
  107. #self.canvas.connect('configure-event', self.auto_adjust_axes)
  108. self.canvas.mpl_connect('resize_event', self.auto_adjust_axes)
  109. #self.canvas.add_events(Gdk.EventMask.SMOOTH_SCROLL_MASK)
  110. #self.canvas.connect("scroll-event", self.on_scroll)
  111. self.canvas.mpl_connect('scroll_event', self.on_scroll)
  112. self.canvas.mpl_connect('key_press_event', self.on_key_down)
  113. self.canvas.mpl_connect('key_release_event', self.on_key_up)
  114. self.canvas.mpl_connect('draw_event', self.on_draw)
  115. self.mouse = [0, 0]
  116. self.key = None
  117. self.pan_axes = []
  118. self.panning = False
  119. def on_new_screen(self):
  120. log.debug("Cache updated the screen!")
  121. def on_key_down(self, event):
  122. """
  123. :param event:
  124. :return:
  125. """
  126. FlatCAMApp.App.log.debug('on_key_down(): ' + str(event.key))
  127. self.key = event.key
  128. def on_key_up(self, event):
  129. """
  130. :param event:
  131. :return:
  132. """
  133. self.key = None
  134. def mpl_connect(self, event_name, callback):
  135. """
  136. Attach an event handler to the canvas through the Matplotlib interface.
  137. :param event_name: Name of the event
  138. :type event_name: str
  139. :param callback: Function to call
  140. :type callback: func
  141. :return: Connection id
  142. :rtype: int
  143. """
  144. return self.canvas.mpl_connect(event_name, callback)
  145. def mpl_disconnect(self, cid):
  146. """
  147. Disconnect callback with the give id.
  148. :param cid: Callback id.
  149. :return: None
  150. """
  151. self.canvas.mpl_disconnect(cid)
  152. def connect(self, event_name, callback):
  153. """
  154. Attach an event handler to the canvas through the native Qt interface.
  155. :param event_name: Name of the event
  156. :type event_name: str
  157. :param callback: Function to call
  158. :type callback: function
  159. :return: Nothing
  160. """
  161. self.canvas.connect(event_name, callback)
  162. def clear(self):
  163. """
  164. Clears axes and figure.
  165. :return: None
  166. """
  167. # Clear
  168. self.axes.cla()
  169. try:
  170. self.figure.clf()
  171. except KeyError:
  172. FlatCAMApp.App.log.warning("KeyError in MPL figure.clf()")
  173. # Re-build
  174. self.figure.add_axes(self.axes)
  175. self.axes.set_aspect(1)
  176. self.axes.grid(True)
  177. # Re-draw
  178. self.canvas.draw_idle()
  179. def adjust_axes(self, xmin, ymin, xmax, ymax):
  180. """
  181. Adjusts all axes while maintaining the use of the whole canvas
  182. and an aspect ratio to 1:1 between x and y axes. The parameters are an original
  183. request that will be modified to fit these restrictions.
  184. :param xmin: Requested minimum value for the X axis.
  185. :type xmin: float
  186. :param ymin: Requested minimum value for the Y axis.
  187. :type ymin: float
  188. :param xmax: Requested maximum value for the X axis.
  189. :type xmax: float
  190. :param ymax: Requested maximum value for the Y axis.
  191. :type ymax: float
  192. :return: None
  193. """
  194. # FlatCAMApp.App.log.debug("PC.adjust_axes()")
  195. width = xmax - xmin
  196. height = ymax - ymin
  197. try:
  198. r = width / height
  199. except ZeroDivisionError:
  200. FlatCAMApp.App.log.error("Height is %f" % height)
  201. return
  202. canvas_w, canvas_h = self.canvas.get_width_height()
  203. canvas_r = float(canvas_w) / canvas_h
  204. x_ratio = float(self.x_margin) / canvas_w
  205. y_ratio = float(self.y_margin) / canvas_h
  206. if r > canvas_r:
  207. ycenter = (ymin + ymax) / 2.0
  208. newheight = height * r / canvas_r
  209. ymin = ycenter - newheight / 2.0
  210. ymax = ycenter + newheight / 2.0
  211. else:
  212. xcenter = (xmax + xmin) / 2.0
  213. newwidth = width * canvas_r / r
  214. xmin = xcenter - newwidth / 2.0
  215. xmax = xcenter + newwidth / 2.0
  216. # Adjust axes
  217. for ax in self.figure.get_axes():
  218. if ax._label != 'base':
  219. ax.set_frame_on(False) # No frame
  220. ax.set_xticks([]) # No tick
  221. ax.set_yticks([]) # No ticks
  222. ax.patch.set_visible(False) # No background
  223. ax.set_aspect(1)
  224. ax.set_xlim((xmin, xmax))
  225. ax.set_ylim((ymin, ymax))
  226. ax.set_position([x_ratio, y_ratio, 1 - 2 * x_ratio, 1 - 2 * y_ratio])
  227. # Sync re-draw to proper paint on form resize
  228. self.canvas.draw()
  229. ##### Temporary place-holder for cached update #####
  230. self.update_screen_request.emit([0, 0, 0, 0, 0])
  231. def auto_adjust_axes(self, *args):
  232. """
  233. Calls ``adjust_axes()`` using the extents of the base axes.
  234. :rtype : None
  235. :return: None
  236. """
  237. xmin, xmax = self.axes.get_xlim()
  238. ymin, ymax = self.axes.get_ylim()
  239. self.adjust_axes(xmin, ymin, xmax, ymax)
  240. def zoom(self, factor, center=None):
  241. """
  242. Zooms the plot by factor around a given
  243. center point. Takes care of re-drawing.
  244. :param factor: Number by which to scale the plot.
  245. :type factor: float
  246. :param center: Coordinates [x, y] of the point around which to scale the plot.
  247. :type center: list
  248. :return: None
  249. """
  250. xmin, xmax = self.axes.get_xlim()
  251. ymin, ymax = self.axes.get_ylim()
  252. width = xmax - xmin
  253. height = ymax - ymin
  254. if center is None or center == [None, None]:
  255. center = [(xmin + xmax) / 2.0, (ymin + ymax) / 2.0]
  256. # For keeping the point at the pointer location
  257. relx = (xmax - center[0]) / width
  258. rely = (ymax - center[1]) / height
  259. new_width = width / factor
  260. new_height = height / factor
  261. xmin = center[0] - new_width * (1 - relx)
  262. xmax = center[0] + new_width * relx
  263. ymin = center[1] - new_height * (1 - rely)
  264. ymax = center[1] + new_height * rely
  265. # Adjust axes
  266. for ax in self.figure.get_axes():
  267. ax.set_xlim((xmin, xmax))
  268. ax.set_ylim((ymin, ymax))
  269. # Async re-draw
  270. self.canvas.draw_idle()
  271. ##### Temporary place-holder for cached update #####
  272. self.update_screen_request.emit([0, 0, 0, 0, 0])
  273. def pan(self, x, y):
  274. xmin, xmax = self.axes.get_xlim()
  275. ymin, ymax = self.axes.get_ylim()
  276. width = xmax - xmin
  277. height = ymax - ymin
  278. # Adjust axes
  279. for ax in self.figure.get_axes():
  280. ax.set_xlim((xmin + x * width, xmax + x * width))
  281. ax.set_ylim((ymin + y * height, ymax + y * height))
  282. # Re-draw
  283. self.canvas.draw_idle()
  284. ##### Temporary place-holder for cached update #####
  285. self.update_screen_request.emit([0, 0, 0, 0, 0])
  286. def new_axes(self, name):
  287. """
  288. Creates and returns an Axes object attached to this object's Figure.
  289. :param name: Unique label for the axes.
  290. :return: Axes attached to the figure.
  291. :rtype: Axes
  292. """
  293. return self.figure.add_axes([0.05, 0.05, 0.9, 0.9], label=name)
  294. def on_scroll(self, event):
  295. """
  296. Scroll event handler.
  297. :param event: Event object containing the event information.
  298. :return: None
  299. """
  300. # So it can receive key presses
  301. # self.canvas.grab_focus()
  302. self.canvas.setFocus()
  303. # Event info
  304. # z, direction = event.get_scroll_direction()
  305. if self.key is None:
  306. if event.button == 'up':
  307. self.zoom(1.5, self.mouse)
  308. else:
  309. self.zoom(1 / 1.5, self.mouse)
  310. return
  311. if self.key == 'shift':
  312. if event.button == 'up':
  313. self.pan(0.3, 0)
  314. else:
  315. self.pan(-0.3, 0)
  316. return
  317. if self.key == 'control':
  318. if event.button == 'up':
  319. self.pan(0, 0.3)
  320. else:
  321. self.pan(0, -0.3)
  322. return
  323. def on_mouse_press(self, event):
  324. # Check for middle mouse button press
  325. if event.button == 2:
  326. # Prepare axes for pan (using 'matplotlib' pan function)
  327. self.pan_axes = []
  328. for a in self.figure.get_axes():
  329. if (event.x is not None and event.y is not None and a.in_axes(event) and
  330. a.get_navigate() and a.can_pan()):
  331. a.start_pan(event.x, event.y, 1)
  332. self.pan_axes.append(a)
  333. # Set pan view flag
  334. if len(self.pan_axes) > 0: self.panning = True;
  335. def on_mouse_release(self, event):
  336. # Check for middle mouse button release to complete pan procedure
  337. if event.button == 2:
  338. for a in self.pan_axes:
  339. a.end_pan()
  340. # Clear pan flag
  341. self.panning = False
  342. def on_mouse_move(self, event):
  343. """
  344. Mouse movement event hadler. Stores the coordinates. Updates view on pan.
  345. :param event: Contains information about the event.
  346. :return: None
  347. """
  348. self.mouse = [event.xdata, event.ydata]
  349. # Update pan view on mouse move
  350. if self.panning is True:
  351. for a in self.pan_axes:
  352. a.drag_pan(1, event.key, event.x, event.y)
  353. # Async re-draw (redraws only on thread idle state, uses timer on backend)
  354. self.canvas.draw_idle()
  355. ##### Temporary place-holder for cached update #####
  356. self.update_screen_request.emit([0, 0, 0, 0, 0])
  357. def on_draw(self, renderer):
  358. # Store background on canvas redraw
  359. self.background = self.canvas.copy_from_bbox(self.axes.bbox)
  360. def get_axes_pixelsize(self):
  361. """
  362. Axes size in pixels.
  363. :return: Pixel width and height
  364. :rtype: tuple
  365. """
  366. bbox = self.axes.get_window_extent().transformed(self.figure.dpi_scale_trans.inverted())
  367. width, height = bbox.width, bbox.height
  368. width *= self.figure.dpi
  369. height *= self.figure.dpi
  370. return width, height
  371. def get_density(self):
  372. """
  373. Returns unit length per pixel on horizontal
  374. and vertical axes.
  375. :return: X and Y density
  376. :rtype: tuple
  377. """
  378. xpx, ypx = self.get_axes_pixelsize()
  379. xmin, xmax = self.axes.get_xlim()
  380. ymin, ymax = self.axes.get_ylim()
  381. width = xmax - xmin
  382. height = ymax - ymin
  383. return width / xpx, height / ypx