Source code for pdpy_lib.extra.arranger

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# **************************************************************************** #
# This file is part of the pdpy project: https://github.com/pdpy-org
# Copyright (C) 2022 Fede Camara Halac
# **************************************************************************** #
"""
Arranger
========
"""

from ..primitives.point import Point

__all__ = [ "Arranger" ]

[docs]class Arranger: r""" Arranger objects on a 2d surface This class attempts to arrange objects graphically on the self.canvas. To use, simply import and call the class. The main algorithm consists of initialization and three steps: 1. Initialization 2. step1: :func:`step1` 3. step2: :func:`step2` 4. step3: :func:`step3` Parameters ---------- canvas : :class:`pdpy.Canvas` The canvas to arrange verbose : `bool` (optional) set verbosity level (default: `False`) hstep : `float` (optional) set the horizontal step factor for x-increments (default: `1.5`) This factor multiplies the width maxima of all nodes in a canvas. vstep : `float` (optional) set the vertical step factor for y-increments (default: `1`) This factor multiplies the height maxima of all nodes in a canvas. xmargin : `int` (optional) set the initial x margin on the canvas (default: `10`) xmargin : `int` (optional) set the initial x margin on the canvas (default: `10`) Returns ------- `None` Raises ------ Exception if there were errors during arrangement ValueError if the `Canvas` has no nodes to arrange Example ------- Import the PdPy and Arranger classes >>> from .classes.pdpy import PdPy >>> from .util.arrange import Arranger as arranger Create the PdPy instance (and add some objects) >>> pd = PdPy() Call the function >>> arranger(p) """ def __init__(self, canvas, verbose=False, hstep=1.5, vstep=1, xmargin=10, ymargin=10): # inicializar self.verbose = verbose self.canvas = canvas self.nodes = True self.comments = True if not hasattr(self.canvas, 'nodes') or len(self.canvas.nodes) == 0: self.__print__("Canvas has no nodes.") self.nodes = False if not hasattr(self.canvas, 'comments') or len(self.canvas.comments) == 0: self.__print__("Canvas has no comments.") self.comments = False # the nodes to place if self.nodes and self.comments: self.O = list(self.canvas.nodes) + list(self.canvas.comments) elif self.nodes: self.O = list(self.canvas.nodes) elif self.comments: self.O = list(self.canvas.comments) else: self.__print__("Nothing to arrange.") return # the horizontal step size for increments self.hstep = hstep # the vertical step size for increments self.vstep = vstep # the cursor self.margin = Point(x=xmargin, y=ymargin) self.cursor = Point(x=self.margin.x, y=self.margin.y) self.Z = [] # the placed nodes # Inicializar los maximos de w y h # self.W = max(map( # lambda x:x[0], # [o.__get_obj_size__(self.canvas) for o in self.O] # )) # self.H = max(map( # lambda x:x[1], # [o.__get_obj_size__(self.canvas) for o in self.O] # )) # selfs.y_inc = self.vstep * self.H # self.x_inc = self.hstep * self.W self.__print__("Initialized", __all__[0], "graph placing algorithm.") self.__call__() def __call__(self): self.__print__("========= begin arrange algorithm ==========") try: self.step1() except RecursionError as e: raise Exception("There were errors with the arrangement:", e) self.__print__("------------- end -----------") def __print__(self, *args): if self.verbose: print(*args) def __ids__(self, x): """ Returns the IDs of a list of objects or tuplets of (obj,port) This is useful for printing. """ result = [] for e in x: if isinstance(e, tuple): result.append((e[0].getid(), e[1])) else: result.append(e.getid()) return result def __get_children__(self, o, port=0): """ Returns a the list of children nodes of an object ``o`` Arguments --------- - The second argument is the ``canvas``, of :class:`Canvas` - If ``port`` is set to ``1``, the list contains (obj,port) otherwise, it just contains `(obj)` """ if port: r = [(self.canvas.get(e.sink.getid()), e.source.port) for e in self.canvas.edges if e.source.getid() == o.getid()] self.__print__( "__get_children__():", o.getname(), "==>", list(map(lambda x:[x[0].getid(),x[0].getname(),x[1]], r)) ) return r else: r = [self.canvas.get(e.sink.getid()) for e in self.canvas.edges if e.source.getid() == o.getid()] self.__print__("__get_children__():", self.__ids__(r)) return r def __y_inc__(self, y_inc): """ This increments the Y cursor of a canvas Arguments --------- ``self`` : the canvas of the :class:`PdPy` class ``canvas``: the :class:`Canvas` within ``self`` containing the nodes Returns ------- None """ self.cursor.increment(0, y_inc * self.vstep) def __x_inc__(self, x_inc, port = 0): """ This increments the self.O cursor of a canvas Arguments --------- ``port`` : the port number to offset (default = 0) Returns ------- None """ self.cursor.increment(x_inc * port * self.hstep, 0) def __place__(self, o, xpos=None, ypos=None, yinc=1): """ Place the object on the canvas Parameters ---------- o: pdpy object the object xpos: :class:`int` The position in the x-axis to place the object ypos: :class:`int` The position in the y-axis to place the object yinc: :class:`int` A flag for y-increments to be performed before ``-1``, after ``1``, or not at all ``0`` """ # incrementar Y antes if yinc == -1: self.__y_inc__(self.prev_y_inc) # obtener el tamaño del objeto x_inc, y_inc = o.__get_obj_size__(self.canvas) x_inc += self.margin.x y_inc += self.margin.y x = xpos if xpos is not None else self.cursor.x y = ypos if ypos is not None else self.cursor.y # placelo self.__print__("place():", o.getid(), "=>", x, y) o.addpos(x, y) # incrementar Y despues if yinc == 1: self.__y_inc__(y_inc) # añadirlo a la lista Z self.Z.append(o) # actualizar el tamaño previo self.prev_y_inc, self.prev_x_inc = y_inc, x_inc def __move__(self, o, xoffset, yoffset, x=None, y=None): self.__print__("__move__():", o.getname()) xobj = x if x is not None else o yobj = y if y is not None else o self.__place__(o, xobj.position.x + xoffset, yobj.position.y + yoffset, yinc = 0 ) def __relocator__(self, parent, child): """ Relocates the ``child`` object based on the ``parent`` """ self.__print__("__relocator__()", parent.getid(), child.getid()) self.__print__(parent.position.__pd__(), "and", child.position.__pd__()) if parent in self.__get_children__(child) and child in self.__get_children__(parent): self.__print__("CIRCULAR") self.__move__(parent, self.prev_x_inc, 0, y = child) elif child.position.y != parent.position.y: self.__print__("UNEQUAL_Y") if child.position.y > parent.position.y: self.__print__("child is BELOW the parent") xoff = -abs(child.position.x-parent.position.x) self.__move__(child, xoff, 0, x = parent) else: self.__print__("child is ABOVE the parent") yoff = -abs(child.position.y-parent.position.y) self.__move__(child, 0, yoff, y = parent) elif child.position.y == parent.position.y: self.__print__("EQUAL_Y") self.__move__(child, 0, self.prev_y_inc, y = parent) else: self.__print__("Leaving", child.getid(), "as is.") return self.__print__("--> Relocated to:",child.position.__pd__(), "and", parent.position.__pd__()) return
[docs] def step3(self, obj, children): """ Step3: Place every child For every child in the ``children`` list: #. Adjust y-position #. Adjust x-position #. Place the child #. Pass the child to :func:`step2` with ``relocate=True`` Parameters ---------- obj: :class:`pdpy_lib.core.object.Object` The PdPy object with ``position`` attribute children: ``list`` The list of children whose parent is the passed ``obj`` """ parent = obj self.__print__("="*10,"STEP 3","="*10) self.__print__("input", parent.getid(), parent.getname()) self.__print__(parent.getid(), "is connected to", self.__ids__(children)) self.__print__(">>>>>>>>>> BEGIN CHILD LOOP for", obj.getname()) for i, (child, portnum) in enumerate(children): self.__print__(">"*4,"Child #"+str(i), child.getid(), child.getname(), portnum) self.__print__(">"*4,child.getid(), "NOT IN", self.__ids__(self.Z)) x_inc, y_inc = child.__get_obj_size__(parent.__parent__()) if portnum: self.cursor.y = parent.position.y else: self.__y_inc__(y_inc) self.__x_inc__(x_inc, portnum) self.__print__("PLACEMENT") self.__place__(child, self.cursor.x, self.cursor.y, yinc = 0 ) self.prev_x_inc = x_inc self.prev_y_inc = y_inc self.step2(child, relocate = True) self.__print__("<<<<<<<<<< end child loop for", obj.getname()) self.__print__("-"*10,"(end STEP 3)","-"*10)
[docs] def step2(self, obj, relocate = False): """ Step 2: Take the object's children from the object list This step takes performs the following instructions: #. ``return`` if the `obj` has no children #. otherwise, pop all children from the object list For each child, if the child is not on the object list: #. If the child is not placed, place it, otherwise move it #. If ``relocate`` is ``True``, run the relocator on that child If there are popped children, pass them to :func:`step3`, otherwise #. reset y-position #. and go back to :func:`step1` Parameters ---------- obj: :class:`pdpy.Object` A patchable PdPy object based on `object` with a `position` attribute relocate: ``callback`` or ``None`` A callback function to perform object relocation Returns ------- `None` """ self.__print__("'"*10,"BEGIN STEP 2","'"*10) self.__print__("input", obj.getid(), obj.getname(), relocate.__class__.__name__) if not hasattr(self.canvas, 'edges'): self.__print__(self.canvas.getname(), "has no connections.") return # the actual list of children -> (child,port) children = self.__get_children__(obj, port = True) # no children, so continue with next in line if no relocate if not len(children): return C = [] # the list of children taken from x for (child, port) in children: if child not in self.O: self.__print__(child.getid(), "is not connected to anybody.") if child not in self.Z: self.__print__("But,", child.getid(), "is not placed in", self.__ids__(self.Z)) self.__place__(child, yinc = 1) elif relocate is None: # move the child if there is no relocate callback self.__move__(child, 0, self.y_inc) # also: run the relocator callback if it is there if relocate: self.__relocator__(obj, child) self.__print__(" *** done relocating, continuing") else: self.__print__("child exists, appending", child.getid()) child_index = self.O.index(child) child_from_x = self.O.pop(child_index) C.append((child_from_x, port)) if not len(self.O): break self.__print__("done with child loop", len(C)) self.__print__("`"*10,"(end STEP 2) for ", obj.getname(),"`"*10) if len(C): self.__print__("==> passing to step3") # pasar obj y la lista al paso recursivo self.step3(obj, C) else: x_inc, _ = obj.__get_obj_size__(self.canvas) self.__print__("<== going back to step1") self.__x_inc__(x_inc, 1) self.prev_x_inc = x_inc self.prev_y_inc = 0 self.cursor.y = self.margin.y self.step1()
[docs] def step1(self): """ Step 1: Take the object from the object list This step takes no arguments and performs the following instructions: 1. take an object from the object list 2. place the object 3. pass the object to :func:`step2` without callback If there are no objects on the list, it returns Returns ------- `None` """ self.__print__("~"*10,"STEP 1","~"*10) self.__print__("input",list(zip(map(lambda x:x.getname(),self.O),self.__ids__(self.O)))) if len(self.O) == 0: self.__print__("#2 ===> No more objects to place.") return else: # tomar el primer elemento de self.O obj = self.O.pop(0) # ubicarlo self.__place__(obj) # tomar de self.O todos los elemntos cuyo origen es obj self.step2(obj) self.__print__("-"*10,"(end STEP 1)","-"*10) self.step1()