# coding: utf-8
import types
import numpy as np
import pandas as pd
import pymunk
from . import (svg_polygons_to_df, fit_points_in_bounding_box,
fit_points_in_bounding_box_params)
from .tesselate import tesselate_shapes_frame
from .point_query import get_shapes_pymunk_space
[docs]class ShapesCanvas(object):
'''
The `ShapesCanvas` class fits all shapes defined by vertices in a
`pandas.DataFrame` (one vertex per row) into a specified canvas shape (with
optional padding), while maintaining aspect ratio.
The `ShapesCanvas.find_shape` method returns the shape located at the
specified *canvas* coordinates (or `None`, if no shape intersects with
specified point).
'''
def __init__(self, df_shapes, shape_i_columns, canvas_shape=None,
padding_fraction=0):
'''
Arguments
---------
- `df_shapes`: A `pandas.DataFrame`-like object with `x`, `y` columns,
containing one row per point.
- `shape_i_columns`: Column(s) in `df_shapes` to group points by
shape.
- `canvas_shape`: A `pandas.Series`-like object with a `width` and a
`height`.
'''
self.df_shapes = df_shapes
if isinstance(shape_i_columns, types.StringType):
shape_i_columns = [shape_i_columns]
self.shape_i_columns = shape_i_columns
# Scale and center source points to canvas shape.
self.source_shape = pd.Series(df_shapes[['x', 'y']].max().values,
index=['width', 'height'])
# Tesselate electrode polygons into convex shapes (triangles), for
# compatability with `pymunk`.
self.df_tesselations = tesselate_shapes_frame(self.df_shapes,
shape_i_columns)
# Create `pymunk` space and add a body for each convex shape. Each
# body is mapped to the original shape identifier through
# `canvas_bodies`.
self.space, self.bodies = get_shapes_pymunk_space(self.df_tesselations,
shape_i_columns +
['triangle_i'])
self.padding_fraction = padding_fraction
self.reset_shape(canvas_shape, self.padding_fraction)
[docs] def reset_shape(self, canvas_shape=None, padding_fraction=None):
if canvas_shape is None:
canvas_shape = self.source_shape.copy()
if padding_fraction is None:
padding_fraction = self.padding_fraction
self.canvas_shape = canvas_shape
self.df_canvas_shapes = fit_points_in_bounding_box(self.df_shapes,
canvas_shape,
padding_fraction=
padding_fraction)
# Compute shape (i.e., width and height) of bounding box for each
# canvas shape.
df_bounding_coords = (self.df_canvas_shapes.groupby('id')[['x', 'y']]
.agg(['min', 'max']))
bounding_width = df_bounding_coords.x['max'] - df_bounding_coords.x['min']
bounding_height = df_bounding_coords.y['max'] - df_bounding_coords.y['min']
self.df_bounding_shapes = pd.DataFrame(np.column_stack([bounding_width,
bounding_height]),
columns=['width', 'height'],
index=df_bounding_coords.index)
if self.df_canvas_shapes.shape[0] == 0:
self.canvas_offset = pd.Series([0, 0], index=['x', 'y'])
self.canvas_scale = 1.
else:
# Get x/y-offset and scale for later use.
self.canvas_offset, self.canvas_scale = \
fit_points_in_bounding_box_params(self.df_shapes, canvas_shape,
padding_fraction=
padding_fraction)
# Create transformation matrix to map from shapes coordinate space to
# canvas coordinate space.
self.shapes_to_canvas_transform = get_transform(self.canvas_offset,
self.canvas_scale)
self.canvas_to_shapes_transform = \
np.linalg.inv(self.shapes_to_canvas_transform)
@classmethod
[docs] def from_svg(cls, svg_filepath, *args, **kwargs):
# Read SVG polygons into dataframe, one row per polygon vertex.
df_shapes = svg_polygons_to_df(svg_filepath)
return cls(df_shapes, 'path_id', *args, **kwargs)
[docs] def find_shape(self, canvas_x, canvas_y):
'''
Look up shape based on canvas coordinates.
'''
shape_x, shape_y, w = self.canvas_to_shapes_transform.dot([canvas_x,
canvas_y,
1])
if hasattr(self.space, 'point_query_first'):
# Assume `pymunk<5.0`.
shape = self.space.point_query_first((shape_x, shape_y))
else:
# Assume `pymunk>=5.0`, where `point_query_first` method has been
# deprecated.
info = self.space.point_query_nearest((shape_x, shape_y), 0,
[pymunk.ShapeFilter
.ALL_CATEGORIES])
shape = info.shape if info else None
if shape:
return self.bodies[shape.body]
return None