"""Module containing methods/classes for laying out ancestor trees.
"""
__all__ = ["AncestorTree", "AncestorTreeVisitor", "TreeNode"]
import abc
import logging
from .size import Size
from .textbox import TextBox
_log = logging.getLogger(__name__)
[docs]class TreeNode:
"""Class representing node in a tree, which is a box with a person name.
Parameters
----------
person : `ged4py.model.Individual`
Corresponding individual, may be ``None``.
gen : `int`
Generation number, 0 for the tree root.
motherNode : `TreeNode`
Node for mother, can be ``None``.
fatherNode : `TreeNode`
Node for father, can be ``None``.
box_width : `ged2doc.size.Size`
Desired width of this node, actual width can grow.
max_box_width : `ged2doc.size.Size`
Maximum width this node can grow to.
font_size : `ged2doc.size.Size`
Size of the font for the text.
gen_dist : `ged2doc.size.Size`
Horiz. distance between generations.
"""
_vpadding = Size('2pt') # vertical padding around each sub-tree or node
def __init__(self, person, gen, motherNode, fatherNode, box_width,
max_box_width, font_size, gen_dist):
self.mother = motherNode
self.father = fatherNode
self.generation = gen
self._person = person
# displayed persons name
if person is None:
self.name = '?'
elif gen == 0:
self.name = (person.name.first or '') + ' ' + \
(person.name.maiden or person.name.surname or '')
if not self.name.strip():
self.name = '...'
else:
self.name = (person.name.first or '') + ' ' + \
(person.name.surname or '')
href = None if person is None else ('#person.' + person.xref_id)
x0 = gen * (gen_dist + box_width)
self._box = TextBox(text=self.name, x0=x0, width=box_width,
maxwidth=max_box_width, font_size=font_size,
href=href)
self.setY0(Size())
@property
def person(self):
"""Person corresponding to this node, can be None (`ged4py.model.Individual`).
"""
return self._person
@property
def textbox(self):
"""Textbox for this node (`TextBox`).
"""
return self._box
@property
def subTreeHeight(self):
"""The height of the whole tree including parent boxes (`Size`).
"""
h = Size()
if self.mother:
h = self.mother.subTreeHeight + self.father.subTreeHeight + self._vpadding
h = max(h, self._box.height)
_log.debug('TreeNode.name = %s; height = %s', self.name, h)
return h
[docs] def setY0(self, y0):
"""Recalculate Y position of box tree so that topmost box is at `y0`.
Parameters
----------
y0 : `ged2doc.size.Size`
New topmost box position, accepts anything convertible to
`ged2doc.size.Size`.
"""
y0 = Size(y0)
_log.debug('TreeNode.name = %s; setY0 = %s', self.name, y0)
if self.mother:
self.mother.setY0(y0)
self.father.setY0(y0 + self._vpadding + self.mother.subTreeHeight)
# sodd formula need for better precision
self._box.y0 = (2 * self.mother.textbox.y0 + self.mother.textbox.height +
2 * self.father.textbox.y0 + self.father.textbox.height -
2 * self.textbox.height) / 4
else:
self._box.y0 = y0
[docs]class AncestorTree:
"""Class implementing layout of ancestor trees.
Parameters
----------
person : `ged4py.model.Individual`
Corresponding individual, may be ``None``.
max_gen : `int`
Maximum number of generations to plot, default is 4.
width : `ged2doc.size.Size`, optional
Specification for plot width, accepts anything convertible to
`ged2doc.size.Size`.
gen_dist : `ged2doc.size.Size`, optional
Distance between generations, accepts anything convertible to
`ged2doc.size.Size`.
font_size : `ged2doc.size.Size`, optional
Font size, accepts anything convertible to `ged2doc.size.Size`.
"""
def __init__(self, person, max_gen=4, width="5in", gen_dist="12pt", font_size="10pt"):
self.max_gen = max_gen
self._width = Size(width)
self._height = Size()
self.gen_dist = Size(gen_dist)
self.font_size = Size(font_size)
self.root = None
def _genDepth(person, max_gen):
"""Return number known generations for a person"""
if not person:
return 0
if max_gen == 0:
return 0
return max(_genDepth(person.father, max_gen - 1), _genDepth(person.mother, max_gen - 1)) + 1
def _boxes(box):
"""Generator for person parents, returns None for unknown parent"""
yield box
if box.mother:
for p in _boxes(box.mother):
yield p
for p in _boxes(box.father):
yield p
# get the number of generations, limit to max_gen
ngen = _genDepth(person, self.max_gen)
_log.debug('parent_tree: person = %s', person.name)
_log.debug('parent_tree: ngen = %d', ngen)
# if no parents then tree is empty
if ngen < 2:
return
# calculate horizontal size of each box
box_width = (self._width - (self.max_gen - 1) * self.gen_dist -
Size('4pt')) / self.max_gen
max_box_width = (self._width - (ngen - 1) * self.gen_dist -
Size('4pt')) / ngen
# build tree
self.root = self._makeTree(person, 0, ngen, box_width, max_box_width)
# add small padding, get full height
self._height = self.root.subTreeHeight + Size("4pt")
self.root.setY0("2pt")
# update box width for every generation and calculate total width
width = Size('2pt') # extra 1pt to avoid cropping
for gen in range(ngen):
gen_width = max(pbox.textbox.width for pbox in _boxes(self.root)
if pbox.generation == gen)
for pbox in _boxes(self.root):
if pbox.generation == gen:
pbox.textbox.width = gen_width
pbox.textbox.x0 = width
_log.debug('parent_tree: %s', pbox.textbox)
width += gen_width + self.gen_dist
width -= self.gen_dist
width += Size('2pt') # extra 1pt to avoid cropping
self._width = width
_log.debug('parent_tree: size = %s x %s', self._width, self._height)
@property
def width(self):
"""Full width of the tree (`ged2doc.size.Size`)
"""
return self._width
@property
def height(self):
"""Full height of the tree (`ged2doc.size.Size`)
"""
return self._height
[docs] def visit(self, visitor):
"""Visit every node and edge in a tree.
Parameters
----------
visitor : `AncestorTreeVisitor`
Tree visitor.
"""
if self.root:
self._visit(visitor, self.root)
[docs] def _visit(self, visitor, node):
"""Helper method for recursive visiting of the nodes.
"""
visitor.visitNode(node)
if node.mother:
self._visit(visitor, node.mother)
visitor.visitMotherEdge(node, node.mother)
if node.father:
self._visit(visitor, node.father)
visitor.visitFatherEdge(node, node.father)
[docs] def _makeTree(self, person, gen, max_gen, box_width, max_box_width):
"""Recursively generate tree of TreeNode instances.
Fro internal use only.
"""
if gen < max_gen:
motherTree = None
fatherTree = None
if person and (person.mother or person.father):
motherTree = self._makeTree(person.mother, gen + 1, max_gen,
box_width, max_box_width)
fatherTree = self._makeTree(person.father, gen + 1, max_gen,
box_width, max_box_width)
box = TreeNode(person, gen, motherTree, fatherTree, box_width,
max_box_width, self.font_size, self.gen_dist)
return box
[docs]class AncestorTreeVisitor(metaclass=abc.ABCMeta):
"""Interface for tree visitors.
Instances of this class can be passed to `AncestorTree.visit()` method to
iterate over all nodes and edges in an ancestor tree.
"""
[docs] @abc.abstractmethod
def visitNode(self, node):
"""Visitor method for a node in tree.
Parameters
----------
node : `TreeNode`
Tree node.
"""
raise NotImplementedError()
[docs] @abc.abstractmethod
def visitMotherEdge(self, node, parentNode):
"""Visitor method for an edge leading from node to its mother.
It is guaranteed that `visitNode` is called for both nodes before
this method is called.
Parameters
----------
node : `TreeNode`
Tree node.
parentNode : `TreeNode`
Parent tree node.
"""
raise NotImplementedError()
[docs] @abc.abstractmethod
def visitFatherEdge(self, node, parentNode):
"""Visitor method for an edge leading from node to its mother.
It is guaranteed that `visitNode` is called for both nodes before
this method is called.
Parameters
----------
node : `TreeNode`
Tree node.
parentNode : `TreeNode`
Parent tree node.
"""
raise NotImplementedError()