from vispy.visuals import CompoundVisual, LineVisual, MeshVisual from vispy.scene.visuals import create_visual_node from vispy.gloo import set_state from vispy.color import Color from shapely.geometry import Polygon, LineString, LinearRing from multiprocessing import Pool import threading import numpy as np from VisPyTesselators import GLUTess def _update_shape_buffers(data, triangulation='glu'): """ Translates Shapely geometry to internal buffers for speedup redraws :param data: dict Input shape data :param triangulation: str Triangulation engine """ mesh_vertices = [] # Vertices for mesh mesh_tris = [] # Faces for mesh mesh_colors = [] # Face colors line_pts = [] # Vertices for line line_colors = [] # Line color geo, color, face_color, tolerance = data['geometry'], data['color'], data['face_color'], data['tolerance'] if geo is not None and not geo.is_empty: simple = geo.simplify(tolerance) if tolerance else geo # Simplified shape pts = [] # Shape line points tri_pts = [] # Mesh vertices tri_tris = [] # Mesh faces if type(geo) == LineString: # Prepare lines pts = _linestring_to_segments(list(simple.coords)) elif type(geo) == LinearRing: # Prepare lines pts = _linearring_to_segments(list(simple.coords)) elif type(geo) == Polygon: # Prepare polygon faces if face_color is not None: if triangulation == 'glu': gt = GLUTess() tri_tris, tri_pts = gt.triangulate(simple) else: print "Triangulation type '%s' isn't implemented. Drawing only edges." % triangulation # Prepare polygon edges if color is not None: pts = _linearring_to_segments(list(simple.exterior.coords)) for ints in simple.interiors: pts += _linearring_to_segments(list(ints.coords)) # Appending data for mesh if len(tri_pts) > 0 and len(tri_tris) > 0: mesh_tris += tri_tris mesh_vertices += tri_pts mesh_colors += [Color(face_color).rgba] * (len(tri_tris) / 3) # Appending data for line if len(pts) > 0: line_pts += pts line_colors += [Color(color).rgba] * len(pts) # Store buffers data['line_pts'] = line_pts data['line_colors'] = line_colors data['mesh_vertices'] = mesh_vertices data['mesh_tris'] = mesh_tris data['mesh_colors'] = mesh_colors # Clear shapely geometry del data['geometry'] return data def _linearring_to_segments(arr): # Close linear ring """ Translates linear ring to line segments :param arr: numpy.array Array of linear ring vertices :return: numpy.array Line segments """ if arr[0] != arr[-1]: arr.append(arr[0]) return _linestring_to_segments(arr) def _linestring_to_segments(arr): """ Translates line strip to segments :param arr: numpy.array Array of line strip vertices :return: numpy.array Line segments """ return [arr[i / 2] for i in range(0, len(arr) * 2)][1:-1] class ShapeGroup(object): def __init__(self, collection): """ Represents group of shapes in collection :param collection: ShapeCollection Collection to work with """ self._collection = collection self._indexes = [] self._results = {} self._visible = True def add(self, **kwargs): """ Adds shape to collection and store index in group :param kwargs: keyword arguments Arguments for ShapeCollection.add function """ self._indexes.append(self._collection.add(**kwargs)) def clear(self, update=False): """ Removes group shapes from collection, clear indexes :param update: bool Set True to redraw collection """ for i in self._indexes: self._collection.remove(i, False) del self._indexes[:] if update: self._collection.redraw([]) # Skip waiting results def redraw(self): """ Redraws shape collection """ self._collection.redraw(self._indexes) @property def visible(self): """ Visibility of group :return: bool """ return self._visible @visible.setter def visible(self, value): """ Visibility of group :param value: bool """ self._visible = value for i in self._indexes: self._collection.data[i]['visible'] = value self._collection.redraw([]) class ShapeCollectionVisual(CompoundVisual): # Shared multiprocessing pool pool = None def __init__(self, line_width=1, triangulation='gpc', layers=3, **kwargs): """ Represents collection of shapes to draw on VisPy scene :param line_width: float Width of lines/edges :param triangulation: str Triangulation method used for polygons translation 'vispy' - VisPy lib triangulation 'gpc' - Polygon2 lib :param layers: int Layers count Each layer adds 2 visuals on VisPy scene. Be careful: more layers cause less fps :param kwargs: """ self.data = {} self.last_key = -1 self.lock = threading.Lock() if not ShapeCollection.pool: ShapeCollection.pool = Pool() self.results = {} self._meshes = [MeshVisual() for _ in range(0, layers)] self._lines = [LineVisual(antialias=True) for _ in range(0, layers)] self._line_width = line_width self._triangulation = triangulation visuals = [self._lines[i / 2] if i % 2 else self._meshes[i / 2] for i in range(0, layers * 2)] CompoundVisual.__init__(self, visuals, **kwargs) for m in self._meshes: pass m.set_gl_state(polygon_offset_fill=True, polygon_offset=(1, 1), cull_face=False) for l in self._lines: pass l.set_gl_state(blend=True) self.freeze() def add(self, shape=None, color=None, face_color=None, visible=True, update=False, layer=1, tolerance=0.01): """ Adds shape to collection :return: :param shape: shapely.geometry Shapely geometry object :param color: str, tuple Line/edge color :param face_color: str, tuple Polygon face color :param visible: bool Shape visibility :param update: bool Set True to redraw collection :param layer: int Layer number. 0 - lowest. :param tolerance: float Geometry simplifying tolerance :return: int Index of shape """ # Get new key self.lock.acquire(True) self.last_key += 1 key = self.last_key self.lock.release() # Prepare data for translation self.data[key] = {'geometry': shape, 'color': color, 'face_color': face_color, 'visible': visible, 'layer': layer, 'tolerance': tolerance} # Add data to process pool self.results[key] = self.pool.apply_async(_update_shape_buffers, [self.data[key]]) if update: self.redraw() # redraw() waits for pool process end return key def remove(self, key, update=False): """ Removes shape from collection :param key: int Shape index to remove :param update: Set True to redraw collection """ del self.results[key] del self.data[key] if update: self.__update() def clear(self, update=False): """ Removes all shapes from colleciton :param update: bool Set True to redraw collection """ self.data.clear() if update: self.__update() def __update(self): """ Merges internal buffers, sets data to visuals, redraws collection on scene """ mesh_vertices = [[] for _ in range(0, len(self._meshes))] # Vertices for mesh mesh_tris = [[] for _ in range(0, len(self._meshes))] # Faces for mesh mesh_colors = [[] for _ in range(0, len(self._meshes))] # Face colors line_pts = [[] for _ in range(0, len(self._lines))] # Vertices for line line_colors = [[] for _ in range(0, len(self._lines))] # Line color # Merge shapes buffers for data in self.data.values(): if data['visible'] and 'line_pts' in data: try: line_pts[data['layer']] += data['line_pts'] line_colors[data['layer']] += data['line_colors'] mesh_tris[data['layer']] += [x + len(mesh_vertices[data['layer']]) for x in data['mesh_tris']] mesh_vertices[data['layer']] += data['mesh_vertices'] mesh_colors[data['layer']] += data['mesh_colors'] except Exception as e: print "Data error", e # Updating meshes for i, mesh in enumerate(self._meshes): if len(mesh_vertices[i]) > 0: set_state(polygon_offset_fill=False) mesh.set_data(np.asarray(mesh_vertices[i]), np.asarray(mesh_tris[i], dtype=np.uint32) .reshape((-1, 3)), face_colors=np.asarray(mesh_colors[i])) else: mesh.set_data() mesh._bounds_changed() # Updating lines for i, line in enumerate(self._lines): if len(line_pts[i]) > 0: line.set_data(np.asarray(line_pts[i]), np.asarray(line_colors[i]), self._line_width, 'segments') else: line.clear_data() line._bounds_changed() self._bounds_changed() def redraw(self, indexes=None): """ Redraws collection :param indexes: list Shape indexes to get from process pool """ for i in self.data.keys() if not indexes else indexes: try: self.results[i].wait() # Wait for process results if i in self.data: self.data[i] = self.results[i].get() # Store translated data except Exception as e: print e self.__update() ShapeCollection = create_visual_node(ShapeCollectionVisual)