Serge Noiraud ee1d9be898 Narrative web: Ancestor's tree Fix display looks weird (#929)
* Narrative web: Ancestor's tree display looks weird

Solves the following:
- Person boxes overlap
- Some person boxes partially visible or hidden

Fixes #11382

* Narrative web: some cleanup in ancestortree.css

* Narrative web : ancestor tree and long names.

* Adapt ancestor tree css file for all themes
2019-10-17 11:17:31 +11:00

316 lines
10 KiB
Python

# -*- coding: utf-8 -*-
#!/usr/bin/env python
#
# Gramps - a GTK+/GNOME based genealogy program
#
# Copyright (C) 2000-2007 Donald N. Allingham
# Copyright (C) 2007 Johan Gonqvist <johan.gronqvist@gmail.com>
# Copyright (C) 2007-2009 Gary Burton <gary.burton@zen.co.uk>
# Copyright (C) 2007-2009 Stephane Charette <stephanecharette@gmail.com>
# Copyright (C) 2008-2009 Brian G. Matherly
# Copyright (C) 2008 Jason M. Simanek <jason@bohemianalps.com>
# Copyright (C) 2008-2011 Rob G. Healey <robhealey1@gmail.com>
# Copyright (C) 2010 Doug Blank <doug.blank@gmail.com>
# Copyright (C) 2010 Jakim Friant
# Copyright (C) 2010,2015 Serge Noiraud
# Copyright (C) 2011 Tim G L Lyons
# Copyright (C) 2013 Benny Malengier
# Copyright (C) 2018 Paul D.Smith
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
#
import logging
from gramps.gen.const import GRAMPS_LOCALE as glocale
LOG = logging.getLogger(".NarrativeWeb.BuchheimTree")
_ = glocale.translation.sgettext
#------------------------------------------------------------
#
# DrawTree - a Buchheim draw tree which implements the
# tree drawing algorithm of:
#
# Improving Walker's algorithm to Run in Linear Time
# Christoph Buchheim, Michael Juenger, and Sebastian Leipert
#
# Also see:
#
# Positioning Nodes for General Trees
# John Q. Walker II
#
# The following modifications are noted:
#
# - The root node is 'west' according to the later nomenclature
# employed by Walker with the nodes stretching 'east'
# - This reverses the X & Y co-originates of the Buchheim paper
# - The algorithm has been tweaked to track the maximum X and Y
# as 'width' and 'height' to aid later layout
# - The Buchheim examples track a string identifying the actual
# node but this implementation tracks the handle of the
# DB node identifying the person in the Gramps DB. This is done
# to minimize occupancy at any one time.
#------------------------------------------------------------
class DrawTree(object):
def __init__(self, tree, parent=None, depth=0, number=1):
self.coord_x = -1.
self.coord_y = depth
self.width = self.coord_x
self.height = self.coord_y
self.tree = tree
self.children = [DrawTree(c, self, depth+1, i+1)
for i, c
in enumerate(tree.children)]
self.parent = parent
self.thread = None
self.mod = 0
self.ancestor = self
self.change = self.shift = 0
self._lmost_sibling = None
#this is the number of the node in its group of siblings 1..n
self.number = number
def left(self):
"""
Return the left most child if it exists.
"""
return self.thread or len(self.children) and self.children[0]
def right(self):
"""
Return the rightmost child if it exists.
"""
return self.thread or len(self.children) and self.children[-1]
def lbrother(self):
"""
Return the sibling to the left of this one.
"""
brother = None
if self.parent:
for node in self.parent.children:
if node == self:
return brother
else:
brother = node
return brother
def get_lmost_sibling(self):
"""
Return the leftmost sibling.
"""
if not self._lmost_sibling and self.parent and self != \
self.parent.children[0]:
self._lmost_sibling = self.parent.children[0]
return self._lmost_sibling
lmost_sibling = property(get_lmost_sibling)
def __str__(self):
return "%s: x=%s mod=%s" % (self.tree, self.coord_x, self.mod)
def __repr__(self):
return self.__str__()
def handle(self):
"""
Return the handle of the tree, which is whatever we stored as
in the tree to reference out data.
"""
return self.tree.handle
def buchheim(tree, node_width, h_separation, node_height, v_separation):
"""
Calculate the position of elements of the graph given a minimum
generation width separation and minimum generation height separation.
"""
draw_tree = firstwalk(DrawTree(tree), node_height, v_separation)
min_x = second_walk(draw_tree, 0, node_width+h_separation, 0)
if min_x < 0:
third_walk(draw_tree, 0 - min_x)
top = get_min_coord_y(draw_tree)
height = get_max_coord_y(draw_tree)
return (draw_tree, top, height)
def get_min_coord_y(tree, min_value=100.0):
""" Get the minimum coord_y """
if tree.coord_y < min_value:
min_value = tree.coord_y
for child in tree.children:
min_v = get_min_coord_y(child, min_value)
if min_value > min_v:
min_value = min_v
return min_value
def get_max_coord_y(tree, max_value=0.0):
""" Get the maximum coord_y """
if tree.coord_y > max_value:
max_value = tree.coord_y
for child in tree.children:
max_v = get_max_coord_y(child, max_value)
if max_value < max_v:
max_value = max_v
return max_value
def third_walk(tree, adjust):
"""
The tree has have wandered into 'negative' co-ordinates so bring it back
into the piositive domain.
"""
tree.coord_x += adjust
tree.width = max(tree.width, tree.coord_x)
for child in tree.children:
third_walk(child, adjust)
def firstwalk(tree, node_height, v_separation):
"""
Determine horizontal positions.
"""
if not tree.children:
if tree.lmost_sibling:
tree.coord_y = tree.lbrother().coord_y + node_height + v_separation
else:
tree.coord_y = 0.
else:
default_ancestor = tree.children[0]
for child in tree.children:
firstwalk(child, node_height, v_separation)
default_ancestor = apportion(
child, default_ancestor, node_height + v_separation)
tree.height = max(tree.height, child.height)
assert tree.width >= child.width
execute_shifts(tree)
midpoint = (tree.children[0].coord_y + tree.children[-1].coord_y) / 2
brother = tree.lbrother()
if brother:
tree.coord_y = brother.coord_y + node_height + v_separation
tree.mod = tree.coord_y - midpoint
else:
tree.coord_y = midpoint
assert tree.width >= tree.coord_x
tree.height = max(tree.height, tree.coord_y)
return tree
def apportion(tree, default_ancestor, v_separation):
"""
Figure out relative positions of node in a tree.
"""
brother = tree.lbrother()
if brother is not None:
#in buchheim notation:
#i == inner; o == outer; r == right; l == left; r = +; l = -
vir = vor = tree
vil = brother
vol = tree.lmost_sibling
sir = sor = tree.mod
sil = vil.mod
sol = vol.mod
while vil.right() and vir.left():
vil = vil.right()
vir = vir.left()
vol = vol.left()
vor = vor.right()
vor.ancestor = tree
shift = (vil.coord_y + sil) - (vir.coord_y + sir) + v_separation
if shift > 0:
move_subtree(ancestor(
vil, tree, default_ancestor), tree, shift)
sir = sir + shift
sor = sor + shift
sil += vil.mod
sir += vir.mod
sol += vol.mod
sor += vor.mod
if vil.right() and not vor.right():
vor.thread = vil.right()
vor.mod += sil - sor
else:
if vir.left() and not vol.left():
vol.thread = vir.left()
vol.mod += sir - sol
default_ancestor = tree
return default_ancestor
def move_subtree(walk_l, walk_r, shift):
"""
Determine possible shifts required to accomodate new node, but don't
perform the shifts yet.
"""
subtrees = walk_r.number - walk_l.number
# print wl.tree, "is conflicted with", wr.tree, 'moving',
# subtrees, 'shift', shift
# print wl, wr, wr.number, wl.number, shift, subtrees, shift/subtrees
walk_r.change -= shift / subtrees
walk_r.shift += shift
walk_l.change += shift / subtrees
walk_r.coord_y += shift
walk_r.mod += shift
walk_r.height = max(walk_r.height, walk_r.coord_y)
def execute_shifts(tree):
"""
Shift a tree, and it's subtrees, to allow for the placement of a
new tree.
"""
shift = change = 0
for child in tree.children[::-1]:
# print "shift:", child, shift, child.change
child.coord_y += shift
child.mod += shift
change += child.change
shift += child.shift + change
child.height = max(child.height, child.coord_y)
tree.height = max(tree.height, child.height)
def ancestor(vil, tree, default_ancestor):
"""
The relevant text is at the bottom of page 7 of
Improving Walker's Algorithm to Run in Linear Time" by Buchheim et al
"""
if vil.ancestor in tree.parent.children:
return vil.ancestor
return default_ancestor
def second_walk(tree, modifier=0, h_separation=0, width=0, min_x=None):
"""
Note that some of this code is modified to orientate the root node 'west'
instead of 'north' in the Bushheim algorithms.
"""
tree.coord_y += modifier
tree.coord_x += width
if min_x is None or tree.coord_x < min_x:
min_x = tree.coord_x
for child in tree.children:
min_x = second_walk(
child, modifier + tree.mod, h_separation,
width + h_separation, min_x)
tree.width = max(tree.width, child.width)
tree.height = max(tree.height, child.height)
tree.width = max(tree.width, tree.coord_x)
tree.height = max(tree.height, tree.coord_y)
return min_x