def reset_canvas(self, width, height): canvas_shape = pd.Series([width, height], index=['width', 'height']) if self.canvas is None or self.canvas.df_shapes.shape[0] == 0: self.canvas = ShapesCanvas(self.df_shapes, self.shape_i_columns, canvas_shape=canvas_shape, padding_fraction=self.padding_fraction) else: self.canvas.reset_shape(canvas_shape)
def __init__(self, svg_filepath, name=None, **kwargs): self.name = name or path(svg_filepath).namebase # Read SVG paths and polygons from `Device` layer into data frame, one # row per polygon vertex. self.df_shapes = svg_shapes_to_df(svg_filepath, xpath=ELECTRODES_XPATH) # Add SVG file path as attribute. self.svg_filepath = svg_filepath self.shape_i_columns = 'id' # Create temporary shapes canvas with same scale as original shapes # frame. This canvas is used for to conduct point queries to detect # which shape (if any) overlaps with the endpoint of a connection line. svg_canvas = ShapesCanvas(self.df_shapes, self.shape_i_columns) # Detect connected shapes based on lines in "Connection" layer of the # SVG. self.df_shape_connections = extract_connections(self.svg_filepath, svg_canvas) # Scale coordinates to millimeter units. self.df_shapes[['x', 'y']] -= self.df_shapes[['x', 'y']].min().values self.df_shapes[['x', 'y']] /= INKSCAPE_PPmm.magnitude self.df_shapes = compute_shape_centers(self.df_shapes, self.shape_i_columns) self.df_electrode_channels = self.get_electrode_channels() self.graph = nx.Graph() for index, row in self.df_shape_connections.iterrows(): self.graph.add_edge(row['source'], row['target']) # Get data frame, one row per electrode, indexed by electrode path id, # each row denotes electrode center coordinates. self.df_shape_centers = (self.df_shapes.drop_duplicates(subset=['id']) .set_index('id')[['x_center', 'y_center']]) (self.adjacency_matrix, self.indexed_shapes, self.shape_indexes) = get_adjacency_matrix(self.df_shape_connections) self.df_indexed_shape_centers = (self.df_shape_centers .loc[self.shape_indexes.index] .reset_index()) self.df_indexed_shape_centers.rename(columns={'index': 'shape_id'}, inplace=True) self.df_shape_connections_indexed = self.df_shape_connections.copy() self.df_shape_connections_indexed['source'] = \ map(str, self.shape_indexes[self.df_shape_connections['source']]) self.df_shape_connections_indexed['target'] \ = map(str, self.shape_indexes[self.df_shape_connections ['target']]) self.df_shapes_indexed = self.df_shapes.copy() self.df_shapes_indexed['id'] = map(str, self.shape_indexes [self.df_shapes['id']]) # Modified state (`True` if electrode channels have been updated). self._dirty = False
class GtkShapesCanvasView(GtkCairoView): def __init__(self, df_shapes, shape_i_columns, padding_fraction=0, **kwargs): self.canvas = None self.df_shapes = df_shapes self.shape_i_columns = shape_i_columns self.padding_fraction = padding_fraction self._canvas_reset_request = None self.cairo_surface = None self.df_surfaces = pd.DataFrame(None, columns=['name', 'surface']) # Markers to indicate whether drawing needs to be resized, re-rendered, # and/or redrawn. self._dirty_size = None # Either `None` or `(width, height)` self._dirty_render = False self._dirty_draw = False self._dirty_check_timeout_id = None # Periodic callback identifier super(GtkShapesCanvasView, self).__init__(**kwargs) @classmethod def from_svg(cls, svg_filepath, **kwargs): df_shapes = svg_polygons_to_df(svg_filepath) return cls(df_shapes, 'path_id', **kwargs) def create_ui(self): """ .. versionchanged:: 0.20 Debounce window expose and resize handlers to improve responsiveness. .. versionchanged:: X.X.X Call debounced `_on_expose_event` handler on _leading_ edge to make UI update more responsive when, e.g., changing window focus. Decrease debounce time to 250 ms. """ super(GtkShapesCanvasView, self).create_ui() self.widget.set_events(Gdk.EventType.BUTTON_PRESS | Gdk.EventType.BUTTON_RELEASE | Gdk.EventMask.BUTTON_MOTION_MASK | Gdk.EventMask.BUTTON_PRESS_MASK | Gdk.EventMask.BUTTON_RELEASE_MASK | Gdk.EventType.POINTER_MOTION_HINT_MASK) self._dirty_check_timeout_id = GLib.timeout_add(30, self.check_dirty) self.resize = Debounce(self._resize, wait=250) debounced_on_expose_event = Debounce(self._on_expose_event, wait=250, leading=True, trailing=True) self.widget.connect('expose-event', debounced_on_expose_event) # self.widget.connect('expose-event', self._on_expose_event) def reset_canvas(self, width, height): canvas_shape = pd.Series([width, height], index=['width', 'height']) if self.canvas is None or self.canvas.df_shapes.shape[0] == 0: self.canvas = ShapesCanvas(self.df_shapes, self.shape_i_columns, canvas_shape=canvas_shape, padding_fraction=self.padding_fraction) else: self.canvas.reset_shape(canvas_shape) def draw(self): if self.cairo_surface is not None: logger.debug('Paint canvas on widget Cairo surface.') cairo_context = self.widget.window.cairo_create() cairo_context.set_source_surface(self.cairo_surface, 0, 0) cairo_context.paint() else: logger.debug('No Cairo surface to paint to.') def render(self): self.df_surfaces = pd.DataFrame( [['shapes', self.render_shapes()]], columns=['name', 'surface']) self.cairo_surface = flatten_surfaces(self.df_surfaces) def check_dirty(self): """ .. versionchanged:: 0.20 Do not log size change. """ if self._dirty_size is None: if self._dirty_render: self.render() self._dirty_render = False if self._dirty_draw: self.draw() self._dirty_draw = False return True width, height = self._dirty_size self._dirty_size = None self.reset_canvas(width, height) self._dirty_render = True self._dirty_draw = True return True def on_widget__configure_event(self, widget, event): """ Called when size of drawing area changes. """ if event.x < 0 and event.y < 0: # Widget has not been allocated a size yet, so do nothing. return self.resize(event.width, event.height) def _resize(self, width, height): """ .. versionadded:: 0.20 Clear canvas, draw frame off screen, and mark dirty. ..notes:: This method is debounced to improve responsiveness. """ self._dirty_size = width, height self.reset_canvas(width, height) self.draw() self._dirty_draw = True def _on_expose_event(self, widget, event): """ .. versionchanged:: 0.20 Renamed from ``on_widget__expose_event`` to allow wrapping for debouncing to improve responsiveness. Called when drawing area is first displayed and, for example, when part of drawing area is uncovered after being covered up by another window. Clear canvas, draw frame off screen, and mark dirty. """ logger.info('on_widget__expose_event') # Request immediate paint of pre-rendered off-screen Cairo surface to # drawing area widget, but also mark as dirty to redraw after next # render. self.draw() self._dirty_draw = True ########################################################################### # Render methods def get_surface(self, format_=cairo.FORMAT_ARGB32): x, y, width, height = self.widget.get_allocation() surface = cairo.ImageSurface(format_, width, height) return surface def render_shapes(self, df_shapes=None, clip=False): surface = self.get_surface() cairo_context = cairo.Context(surface) if df_shapes is None: df_shapes = self.canvas.df_canvas_shapes for path_id, df_path_i in (df_shapes.groupby( self.canvas.shape_i_columns)[['x', 'y']]): cairo_context.move_to(*df_path_i.iloc[0][['x', 'y']]) for i, (x, y) in df_path_i[['x', 'y']].iloc[1:].iterrows(): cairo_context.line_to(x, y) cairo_context.close_path() cairo_context.set_source_rgb(0, 0, 1) cairo_context.fill() return surface def render_label(self, cairo_context, shape_id, text=None, label_scale=.9): """ Draw label on specified shape. Parameters ---------- cairo_context : cairo.Context Cairo context to draw text width. Can be preconfigured, for example, to set font style, etc. shape_id : str Shape identifier. text : str, optional Label text. If not specified, shape identifier is used. label_scale : float, optional Fraction of limiting dimension of shape bounding box to scale text to. """ text = shape_id if text is None else text shape = self.canvas.df_bounding_shapes.ix[shape_id] shape_center = self.canvas.df_shape_centers.ix[shape_id] font_size, text_shape = \ aspect_fit_font_size(text, shape * label_scale, cairo_context=cairo_context) cairo_context.set_font_size(font_size) cairo_context.move_to(shape_center[0] - .5 * text_shape.width, shape_center[1] + .5 * text_shape.height) cairo_context.show_text(text) def render_labels(self, labels, color_rgba=None): surface = self.get_surface() if self.canvas is None or not hasattr(self.canvas, 'df_shape_centers'): # Canvas is not initialized, so return empty cairo surface. return surface cairo_context = cairo.Context(surface) color_rgba = (1, 1, 1, 1) if color_rgba is None else color_rgba if not isinstance(color_rgba, pd.Series): shape_rgba_colors = pd.Series( [color_rgba], index=self.canvas.df_shape_centers.index) else: shape_rgba_colors = color_rgba font_options = cairo.FontOptions() font_options.set_antialias(cairo.ANTIALIAS_SUBPIXEL) cairo_context.set_font_options(font_options) for shape_id, label_i in labels.iteritems(): cairo_context.set_source_rgba(*shape_rgba_colors.ix[shape_id]) self.render_label(cairo_context, shape_id, label_i, label_scale=0.6) return surface
class GtkShapesCanvasView(GtkCairoView): def __init__(self, df_shapes, shape_i_columns, padding_fraction=0, **kwargs): self.canvas = None self.df_shapes = df_shapes self.shape_i_columns = shape_i_columns self.padding_fraction = padding_fraction self._canvas_reset_request = None self.cairo_surface = None self.df_surfaces = pd.DataFrame(None, columns=['name', 'surface']) # Markers to indicate whether drawing needs to be resized, re-rendered, # and/or redrawn. self._dirty_size = None # Either `None` or `(width, height)` self._dirty_render = False self._dirty_draw = False self._dirty_check_timeout_id = None # Periodic callback identifier super(GtkShapesCanvasView, self).__init__(**kwargs) @classmethod def from_svg(cls, svg_filepath, **kwargs): df_shapes = svg_polygons_to_df(svg_filepath) return cls(df_shapes, 'path_id', **kwargs) def create_ui(self): ''' .. versionchanged:: 0.20 Debounce window expose and resize handlers to improve responsiveness. .. versionchanged:: X.X.X Call debounced `_on_expose_event` handler on _leading_ edge to make UI update more responsive when, e.g., changing window focus. Decrease debounce time to 250 ms. ''' super(GtkShapesCanvasView, self).create_ui() self.widget.set_events(gtk.gdk.BUTTON_PRESS | gtk.gdk.BUTTON_RELEASE | gtk.gdk.BUTTON_MOTION_MASK | gtk.gdk.BUTTON_PRESS_MASK | gtk.gdk.BUTTON_RELEASE_MASK | gtk.gdk.POINTER_MOTION_HINT_MASK) self._dirty_check_timeout_id = gtk.timeout_add(30, self.check_dirty) self.resize = Debounce(self._resize, wait=250) debounced_on_expose_event = Debounce(self._on_expose_event, wait=250, leading=True, trailing=True) self.widget.connect('expose-event', debounced_on_expose_event) # self.widget.connect('expose-event', self._on_expose_event) def reset_canvas(self, width, height): canvas_shape = pd.Series([width, height], index=['width', 'height']) if self.canvas is None or self.canvas.df_shapes.shape[0] == 0: self.canvas = ShapesCanvas(self.df_shapes, self.shape_i_columns, canvas_shape=canvas_shape, padding_fraction=self.padding_fraction) else: self.canvas.reset_shape(canvas_shape) def draw(self): if self.cairo_surface is not None: logger.debug('Paint canvas on widget Cairo surface.') cairo_context = self.widget.window.cairo_create() cairo_context.set_source_surface(self.cairo_surface, 0, 0) cairo_context.paint() else: logger.debug('No Cairo surface to paint to.') def render(self): self.df_surfaces = pd.DataFrame([['shapes', self.render_shapes()]], columns=['name', 'surface']) self.cairo_surface = flatten_surfaces(self.df_surfaces) def check_dirty(self): ''' .. versionchanged:: 0.20 Do not log size change. ''' if self._dirty_size is None: if self._dirty_render: self.render() self._dirty_render = False if self._dirty_draw: self.draw() self._dirty_draw = False return True width, height = self._dirty_size self._dirty_size = None self.reset_canvas(width, height) self._dirty_render = True self._dirty_draw = True return True def on_widget__configure_event(self, widget, event): ''' Called when size of drawing area changes. ''' if event.x < 0 and event.y < 0: # Widget has not been allocated a size yet, so do nothing. return self.resize(event.width, event.height) def _resize(self, width, height): ''' .. versionadded:: 0.20 Clear canvas, draw frame off screen, and mark dirty. ..notes:: This method is debounced to improve responsiveness. ''' self._dirty_size = width, height self.reset_canvas(width, height) self.draw() self._dirty_draw = True def _on_expose_event(self, widget, event): ''' .. versionchanged:: 0.20 Renamed from ``on_widget__expose_event`` to allow wrapping for debouncing to improve responsiveness. Called when drawing area is first displayed and, for example, when part of drawing area is uncovered after being covered up by another window. Clear canvas, draw frame off screen, and mark dirty. ''' logger.info('on_widget__expose_event') # Request immediate paint of pre-rendered off-screen Cairo surface to # drawing area widget, but also mark as dirty to redraw after next # render. self.draw() self._dirty_draw = True ########################################################################### # Render methods def get_surface(self, format_=cairo.FORMAT_ARGB32): x, y, width, height = self.widget.get_allocation() surface = cairo.ImageSurface(format_, width, height) return surface def render_shapes(self, df_shapes=None, clip=False): surface = self.get_surface() cairo_context = cairo.Context(surface) if df_shapes is None: df_shapes = self.canvas.df_canvas_shapes for path_id, df_path_i in (df_shapes .groupby(self.canvas .shape_i_columns)[['x', 'y']]): cairo_context.move_to(*df_path_i.iloc[0][['x', 'y']]) for i, (x, y) in df_path_i[['x', 'y']].iloc[1:].iterrows(): cairo_context.line_to(x, y) cairo_context.close_path() cairo_context.set_source_rgb(0, 0, 1) cairo_context.fill() return surface def render_label(self, cairo_context, shape_id, text=None, label_scale=.9): ''' Draw label on specified shape. Parameters ---------- cairo_context : cairo.Context Cairo context to draw text width. Can be preconfigured, for example, to set font style, etc. shape_id : str Shape identifier. text : str, optional Label text. If not specified, shape identifier is used. label_scale : float, optional Fraction of limiting dimension of shape bounding box to scale text to. ''' text = shape_id if text is None else text shape = self.canvas.df_bounding_shapes.ix[shape_id] shape_center = self.canvas.df_shape_centers.ix[shape_id] font_size, text_shape = \ aspect_fit_font_size(text, shape * label_scale, cairo_context=cairo_context) cairo_context.set_font_size(font_size) cairo_context.move_to(shape_center[0] - .5 * text_shape.width, shape_center[1] + .5 * text_shape.height) cairo_context.show_text(text) def render_labels(self, labels, color_rgba=None): surface = self.get_surface() if self.canvas is None or not hasattr(self.canvas, 'df_shape_centers'): # Canvas is not initialized, so return empty cairo surface. return surface cairo_context = cairo.Context(surface) color_rgba = (1, 1, 1, 1) if color_rgba is None else color_rgba if not isinstance(color_rgba, pd.Series): shape_rgba_colors = pd.Series([color_rgba], index=self.canvas.df_shape_centers .index) else: shape_rgba_colors = color_rgba font_options = cairo.FontOptions() font_options.set_antialias(cairo.ANTIALIAS_SUBPIXEL) cairo_context.set_font_options(font_options) for shape_id, label_i in labels.iteritems(): cairo_context.set_source_rgba(*shape_rgba_colors.ix[shape_id]) self.render_label(cairo_context, shape_id, label_i, label_scale=0.6) return surface
class GtkShapesCanvasView(GtkCairoView): def __init__(self, df_shapes, shape_i_columns, padding_fraction=0, **kwargs): self.canvas = None self.df_shapes = df_shapes self.shape_i_columns = shape_i_columns self.padding_fraction = padding_fraction self._canvas_reset_request = None self.cairo_surface = None self.df_surfaces = pd.DataFrame(None, columns=["name", "surface"]) # Markers to indicate whether drawing needs to be resized, re-rendered, # and/or redrawn. self._dirty_size = None # Either `None` or `(width, height)` self._dirty_render = False self._dirty_draw = False self._dirty_check_timeout_id = None # Periodic callback identifier super(GtkShapesCanvasView, self).__init__(**kwargs) @classmethod def from_svg(cls, svg_filepath, **kwargs): df_shapes = svg_polygons_to_df(svg_filepath) return cls(df_shapes, "path_id", **kwargs) def create_ui(self): super(GtkShapesCanvasView, self).create_ui() self.widget.set_events( gtk.gdk.BUTTON_PRESS | gtk.gdk.BUTTON_RELEASE | gtk.gdk.BUTTON_MOTION_MASK | gtk.gdk.BUTTON_PRESS_MASK | gtk.gdk.BUTTON_RELEASE_MASK | gtk.gdk.POINTER_MOTION_HINT_MASK ) self._dirty_check_timeout_id = gtk.timeout_add(30, self.check_dirty) def reset_canvas(self, width, height): canvas_shape = pd.Series([width, height], index=["width", "height"]) if self.canvas is None or self.canvas.df_shapes.shape[0] == 0: self.canvas = ShapesCanvas( self.df_shapes, self.shape_i_columns, canvas_shape=canvas_shape, padding_fraction=self.padding_fraction ) else: self.canvas.reset_shape(canvas_shape) def draw(self): if self.cairo_surface is not None: logger.debug("Paint canvas on widget Cairo surface.") cairo_context = self.widget.window.cairo_create() cairo_context.set_source_surface(self.cairo_surface, 0, 0) cairo_context.paint() else: logger.debug("No Cairo surface to paint to.") def render(self): self.df_surfaces = pd.DataFrame([["shapes", self.render_shapes()]], columns=["name", "surface"]) self.cairo_surface = flatten_surfaces(self.df_surfaces) def check_dirty(self): if self._dirty_size is None: if self._dirty_render: self.render() self._dirty_render = False if self._dirty_draw: self.draw() self._dirty_draw = False return True logger.info("[check_dirty] %s", self._dirty_size) width, height = self._dirty_size self._dirty_size = None self.reset_canvas(width, height) self._dirty_render = True self._dirty_draw = True return True def on_widget__configure_event(self, widget, event): """ Called when size of drawing area changes. """ # logger.info('on_widget__configure_event') if event.x < 0 and event.y < 0: # Widget has not been allocated a size yet, so do nothing. return self._dirty_size = event.width, event.height def on_widget__expose_event(self, widget, event): """ Called when drawing area is first displayed and, for example, when part of drawing area is uncovered after being covered up by another window. """ logger.info("on_widget__expose_event") # Request immediate paint of pre-rendered off-screen Cairo surface to # drawing area widget, but also mark as dirty to redraw after next # render. self.draw() self._dirty_draw = True ########################################################################### # Render methods def get_surface(self, format_=cairo.FORMAT_ARGB32): x, y, width, height = self.widget.get_allocation() surface = cairo.ImageSurface(format_, width, height) return surface def render_shapes(self, df_shapes=None, clip=False): surface = self.get_surface() cairo_context = cairo.Context(surface) if df_shapes is None: df_shapes = self.canvas.df_canvas_shapes for path_id, df_path_i in df_shapes.groupby(self.canvas.shape_i_columns)[["x", "y"]]: cairo_context.move_to(*df_path_i.iloc[0][["x", "y"]]) for i, (x, y) in df_path_i[["x", "y"]].iloc[1:].iterrows(): cairo_context.line_to(x, y) cairo_context.close_path() cairo_context.set_source_rgb(0, 0, 1) cairo_context.fill() return surface def render_label(self, cairo_context, shape_id, text=None, label_scale=0.9): """ Draw label on specified shape. Args: cairo_context (cairo.Context) : Cairo context to draw text width. Can be preconfigured, for example, to set font style, etc. shape_id (str) : Shape identifier. text (str) : Label text. If not specified, shape identifier is used. label_scale (float) : Fraction of limiting dimension of shape bounding box to scale text to. Returns: None """ text = shape_id if text is None else text shape = self.canvas.df_bounding_shapes.ix[shape_id] shape_center = self.canvas.df_shape_centers.ix[shape_id] font_size, text_shape = aspect_fit_font_size(text, shape * label_scale, cairo_context=cairo_context) cairo_context.set_font_size(font_size) cairo_context.move_to(shape_center[0] - 0.5 * text_shape.width, shape_center[1] + 0.5 * text_shape.height) cairo_context.show_text(text) def render_labels(self, labels, color_rgba=None): surface = self.get_surface() if self.canvas is None or not hasattr(self.canvas, "df_shape_centers"): # Canvas is not initialized, so return empty cairo surface. return surface cairo_context = cairo.Context(surface) color_rgba = (1, 1, 1, 1) if color_rgba is None else color_rgba if not isinstance(color_rgba, pd.Series): shape_rgba_colors = pd.Series([color_rgba], index=self.canvas.df_shape_centers.index) else: shape_rgba_colors = color_rgba font_options = cairo.FontOptions() font_options.set_antialias(cairo.ANTIALIAS_SUBPIXEL) cairo_context.set_font_options(font_options) for shape_id, label_i in labels.iteritems(): cairo_context.set_source_rgba(*shape_rgba_colors.ix[shape_id]) self.render_label(cairo_context, shape_id, label_i, label_scale=0.6) return surface