Add compact Ancestry trees using Buchheim/Walker algorithm
This enhancement adds a new 'compact' field to the Narrated Web Report. A compact tree is one that is not a simple binary layout but uses the algorithm of Buchheim/Walker to create a layout that is sensible but also compact. Creating a compact layout is slower than a simple binary tree but the results are significantly improved and do not leave large areas of whitespace where there are no nodes to be shown.
This commit is contained in:
parent
acfbb0a763
commit
03a89c73e3
298
gramps/plugins/webreport/buchheim.py
Normal file
298
gramps/plugins/webreport/buchheim.py
Normal file
@ -0,0 +1,298 @@
|
||||
# -*- 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)
|
||||
|
||||
return draw_tree
|
||||
|
||||
|
||||
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
|
68
gramps/plugins/webreport/layout.py
Normal file
68
gramps/plugins/webreport/layout.py
Normal file
@ -0,0 +1,68 @@
|
||||
# -*- 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-2017 Serge Noiraud
|
||||
# Copyright (C) 2011 Tim G L Lyons
|
||||
# Copyright (C) 2013 Benny Malengier
|
||||
# Copyright (C) 2016 Allen Crider
|
||||
# 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.
|
||||
#
|
||||
|
||||
|
||||
class LayoutTree:
|
||||
"""
|
||||
Narrative Web Page generator.
|
||||
|
||||
Classe:
|
||||
BuchheimTree - A tree suitable for passing to the Bushheim layout
|
||||
algorithm
|
||||
"""
|
||||
def __init__(self, handle, *children):
|
||||
self.handle = handle
|
||||
self.children = []
|
||||
if isinstance(children, list):
|
||||
children = list(children)
|
||||
for parent in children:
|
||||
if parent is not None:
|
||||
self.children.append(parent)
|
||||
|
||||
def __getitem__(self, key):
|
||||
if isinstance(key, (int, slice)):
|
||||
return self.children[key]
|
||||
if isinstance(key, str):
|
||||
for child in self.children:
|
||||
if child.node == key:
|
||||
return child
|
||||
assert "Key not found"
|
||||
return None
|
||||
|
||||
def __iter__(self):
|
||||
return self.children.__iter__()
|
||||
|
||||
def __len__(self):
|
||||
return len(self.children)
|
@ -166,15 +166,36 @@ class MediaPages(BasePage):
|
||||
gc.collect() # Reduce memory usage when many images.
|
||||
if index == media_count:
|
||||
next_ = None
|
||||
elif index < total:
|
||||
next_ = sorted_media_handles[index]
|
||||
elif len(self.unused_media_handles) > 0:
|
||||
next_ = self.unused_media_handles[0]
|
||||
else:
|
||||
next_ = self.unused_media_handles[idx]
|
||||
next_ = None
|
||||
self.mediapage(self.report, title,
|
||||
media_handle,
|
||||
(prev, next_, index, media_count))
|
||||
prev = media_handle
|
||||
handle, (prev, next_, index, media_count))
|
||||
prev = handle
|
||||
step()
|
||||
index += 1
|
||||
idx += 1
|
||||
|
||||
total = len(self.unused_media_handles)
|
||||
idx = 1
|
||||
prev = sorted_media_handles[len(sorted_media_handles)-1]
|
||||
if total > 0:
|
||||
for media_handle in self.unused_media_handles:
|
||||
media = self.r_db.get_media_from_handle(media_handle)
|
||||
gc.collect() # Reduce memory usage when many images.
|
||||
if index == media_count:
|
||||
next_ = None
|
||||
else:
|
||||
next_ = self.unused_media_handles[idx]
|
||||
self.mediapage(self.report, title,
|
||||
media_handle,
|
||||
(prev, next_, index, media_count))
|
||||
prev = media_handle
|
||||
step()
|
||||
index += 1
|
||||
idx += 1
|
||||
|
||||
self.medialistpage(self.report, title, sorted_media_handles)
|
||||
|
||||
|
@ -289,10 +289,11 @@ class NavWebReport(Report):
|
||||
os.mkdir(dir_name)
|
||||
except IOError as value:
|
||||
msg = _("Could not create the directory: %s"
|
||||
) % dir_name + "\n" + value[1]
|
||||
) % dir_name + "\n" + value.strerror
|
||||
self.user.notify_error(msg)
|
||||
return
|
||||
except:
|
||||
except Exception as exception:
|
||||
LOG.exception(exception)
|
||||
msg = _("Could not create the directory: %s") % dir_name
|
||||
self.user.notify_error(msg)
|
||||
return
|
||||
@ -307,12 +308,13 @@ class NavWebReport(Report):
|
||||
os.mkdir(image_dir_name)
|
||||
except IOError as value:
|
||||
msg = _("Could not create the directory: %s"
|
||||
) % image_dir_name + "\n" + value[1]
|
||||
) % image_dir_name + "\n" + value.strerror
|
||||
self.user.notify_error(msg)
|
||||
return
|
||||
except:
|
||||
except Exception as exception:
|
||||
LOG.exception(exception)
|
||||
msg = _("Could not create the directory: %s"
|
||||
) % image_dir_name + "\n" + value[1]
|
||||
) % image_dir_name + "\n" + str(exception)
|
||||
self.user.notify_error(msg)
|
||||
return
|
||||
else:
|
||||
@ -453,7 +455,7 @@ class NavWebReport(Report):
|
||||
if self.archive:
|
||||
self.archive.close()
|
||||
|
||||
if len(_WRONGMEDIAPATH) > 0:
|
||||
if _WRONGMEDIAPATH:
|
||||
error = '\n'.join([
|
||||
_('ID=%(grampsid)s, path=%(dir)s') % {
|
||||
'grampsid' : x[0],
|
||||
@ -856,7 +858,7 @@ class NavWebReport(Report):
|
||||
@param: bkref_class -- The class associated to this handle (source)
|
||||
@param: bkref_handle -- The handle associated to this source
|
||||
"""
|
||||
if len(self.obj_dict[Source][source_handle]) > 0:
|
||||
if self.obj_dict[Source][source_handle]:
|
||||
for bkref in self.bkref_dict[Source][source_handle]:
|
||||
if bkref_handle == bkref[1]:
|
||||
return
|
||||
@ -893,7 +895,7 @@ class NavWebReport(Report):
|
||||
@param: bkref_class -- The class associated to this handle
|
||||
@param: bkref_handle -- The handle associated to this citation
|
||||
"""
|
||||
if len(self.obj_dict[Citation][citation_handle]) > 0:
|
||||
if self.obj_dict[Citation][citation_handle]:
|
||||
for bkref in self.bkref_dict[Citation][citation_handle]:
|
||||
if bkref_handle == bkref[1]:
|
||||
return
|
||||
@ -926,7 +928,7 @@ class NavWebReport(Report):
|
||||
@param: bkref_class -- The class associated to this handle (media)
|
||||
@param: bkref_handle -- The handle associated to this media
|
||||
"""
|
||||
if len(self.obj_dict[Media][media_handle]) > 0:
|
||||
if self.obj_dict[Media][media_handle]:
|
||||
for bkref in self.bkref_dict[Media][media_handle]:
|
||||
if bkref_handle == bkref[1]:
|
||||
return
|
||||
@ -967,7 +969,7 @@ class NavWebReport(Report):
|
||||
@param: bkref_class -- The class associated to this handle (source)
|
||||
@param: bkref_handle -- The handle associated to this source
|
||||
"""
|
||||
if len(self.obj_dict[Repository][repos_handle]) > 0:
|
||||
if self.obj_dict[Repository][repos_handle]:
|
||||
for bkref in self.bkref_dict[Repository][repos_handle]:
|
||||
if bkref_handle == bkref[1]:
|
||||
return
|
||||
@ -1049,7 +1051,7 @@ class NavWebReport(Report):
|
||||
|
||||
# copy all to images subdir:
|
||||
for from_path in imgs:
|
||||
fdir, fname = os.path.split(from_path)
|
||||
dummy_fdir, fname = os.path.split(from_path)
|
||||
self.copy_file(from_path, fname, "images")
|
||||
|
||||
# copy Gramps marker icon for openstreetmap
|
||||
@ -1126,12 +1128,10 @@ class NavWebReport(Report):
|
||||
len(local_list)) as step:
|
||||
|
||||
SurnameListPage(self, self.title, ind_list,
|
||||
SurnameListPage.ORDER_BY_NAME,
|
||||
self.surname_fname)
|
||||
SurnameListPage.ORDER_BY_NAME, self.surname_fname)
|
||||
|
||||
SurnameListPage(self, self.title, ind_list,
|
||||
SurnameListPage.ORDER_BY_COUNT,
|
||||
"surnames_count")
|
||||
SurnameListPage.ORDER_BY_COUNT, "surnames_count")
|
||||
|
||||
index = 1
|
||||
for (surname, handle_list) in local_list:
|
||||
@ -1493,7 +1493,8 @@ class NavWebReport(Report):
|
||||
try:
|
||||
shutil.copyfile(from_fname, dest)
|
||||
os.utime(dest, (mtime, mtime))
|
||||
except:
|
||||
except Exception as exception:
|
||||
LOG.exception(exception)
|
||||
print("Copying error: %s" % sys.exc_info()[1])
|
||||
print("Continuing...")
|
||||
elif self.warn_dir:
|
||||
@ -1666,9 +1667,10 @@ class NavWebOptions(MenuReportOptions):
|
||||
cright.set_help(_("The copyright to be used for the web files"))
|
||||
addopt("cright", cright)
|
||||
|
||||
self.__css = EnumeratedListOption(_('StyleSheet'), CSS["default"]["id"])
|
||||
for (fname, gid) in sorted([(CSS[key]["translation"], CSS[key]["id"])
|
||||
for key in list(CSS.keys())]):
|
||||
self.__css = EnumeratedListOption(('StyleSheet'), CSS["default"]["id"])
|
||||
for (dummy_fname, gid) in sorted(
|
||||
[(CSS[key]["translation"], CSS[key]["id"])
|
||||
for key in list(CSS.keys())]):
|
||||
if CSS[gid]["user"]:
|
||||
self.__css.add_item(CSS[gid]["id"], CSS[gid]["translation"])
|
||||
self.__css.set_help(_('The stylesheet to be used for the web pages'))
|
||||
@ -1710,10 +1712,11 @@ class NavWebOptions(MenuReportOptions):
|
||||
addopt("ancestortree", self.__ancestortree)
|
||||
self.__ancestortree.connect('value-changed', self.__graph_changed)
|
||||
|
||||
self.__graphgens = NumberOption(_("Graph generations"), 4, 2, 5)
|
||||
self.__graphgens = NumberOption(_("Graph generations"), 4, 2, 10)
|
||||
self.__graphgens.set_help(_("The number of generations to include in "
|
||||
"the ancestor graph"))
|
||||
addopt("graphgens", self.__graphgens)
|
||||
|
||||
self.__graph_changed()
|
||||
|
||||
self.__securesite = BooleanOption(_("This is a secure site (https)"),
|
||||
@ -1726,7 +1729,7 @@ class NavWebOptions(MenuReportOptions):
|
||||
Add more extra pages to the report
|
||||
"""
|
||||
category_name = _("Extra pages")
|
||||
addopt = partial( menu.add_option, category_name )
|
||||
addopt = partial(menu.add_option, category_name)
|
||||
default_path_name = config.get('paths.website-extra-page-name')
|
||||
self.__extra_page_name = StringOption(_("Extra page name"),
|
||||
default_path_name)
|
||||
|
@ -76,6 +76,8 @@ from gramps.plugins.webreport.common import (get_first_letters, _KEYPERSON,
|
||||
MARKER_PATH, OSM_MARKERS,
|
||||
GOOGLE_MAPS, MARKERS, html_escape,
|
||||
DROPMASTERS, FAMILYLINKS)
|
||||
from gramps.plugins.webreport.layout import LayoutTree
|
||||
from gramps.plugins.webreport.buchheim import buchheim
|
||||
|
||||
_ = glocale.translation.sgettext
|
||||
LOG = logging.getLogger(".NarrativeWeb")
|
||||
@ -87,6 +89,8 @@ _VGAP = 10
|
||||
_HGAP = 30
|
||||
_SHADOW = 5
|
||||
_XOFFSET = 5
|
||||
_YOFFSET = 5
|
||||
_LOFFSET = 20
|
||||
|
||||
#################################################
|
||||
#
|
||||
@ -171,7 +175,7 @@ class PersonPages(BasePage):
|
||||
showparents = report.options['showparents']
|
||||
|
||||
output_file, sio = self.report.create_file("individuals")
|
||||
indlistpage, head, body = self.write_header(self._("Individuals"))
|
||||
indlistpage, dummy_head, body = self.write_header(self._("Individuals"))
|
||||
date = 0
|
||||
|
||||
# begin Individuals division
|
||||
@ -330,7 +334,7 @@ class PersonPages(BasePage):
|
||||
family_list = person.get_family_handle_list()
|
||||
first_family = True
|
||||
#partner_name = None
|
||||
tcell = () # pylint: disable=R0204
|
||||
tcell = ()
|
||||
if family_list:
|
||||
for family_handle in family_list:
|
||||
family = self.r_db.get_family_from_handle(
|
||||
@ -568,7 +572,7 @@ class PersonPages(BasePage):
|
||||
individualdetail += self.display_ind_associations(assocs)
|
||||
|
||||
# for use in family map pages...
|
||||
if len(place_lat_long) > 0:
|
||||
if place_lat_long:
|
||||
if self.report.options["familymappages"]:
|
||||
# save output_file, string_io and cur_fname
|
||||
# before creating a new page
|
||||
@ -625,7 +629,7 @@ class PersonPages(BasePage):
|
||||
minx, maxx = Decimal("0.00000001"), Decimal("0.00000001")
|
||||
miny, maxy = Decimal("0.00000001"), Decimal("0.00000001")
|
||||
xwidth, yheight = [], []
|
||||
midx_, midy_, spanx, spany = [None]*4
|
||||
midx_, midy_, dummy_spanx, spany = [None]*4
|
||||
|
||||
number_markers = len(place_lat_long)
|
||||
if number_markers > 1:
|
||||
@ -649,7 +653,7 @@ class PersonPages(BasePage):
|
||||
midx_, midy_ = conv_lat_lon(midx_, midy_, "D.D8")
|
||||
|
||||
# get the integer span of latitude and longitude
|
||||
spanx = int(maxx - minx)
|
||||
dummy_spanx = int(maxx - minx)
|
||||
spany = int(maxy - miny)
|
||||
|
||||
# set zoom level based on span of Longitude?
|
||||
@ -940,17 +944,17 @@ class PersonPages(BasePage):
|
||||
# return family map link to its caller
|
||||
return familymap
|
||||
|
||||
def draw_box(self, center, col, person):
|
||||
def draw_box(self, node, col, person):
|
||||
"""
|
||||
Draw the box around the AncestorTree Individual name box...
|
||||
|
||||
@param: center -- The center of the box
|
||||
draw the box around the AncestorTree Individual name box...
|
||||
@param: node -- The node defining the box location
|
||||
@param: col -- The generation number
|
||||
@param: person -- The person to set in the box
|
||||
"""
|
||||
top = center - _HEIGHT/2
|
||||
xoff = _XOFFSET+col*(_WIDTH+_HGAP)
|
||||
sex = person.gender
|
||||
xoff = _XOFFSET + node.coord_x
|
||||
top = _YOFFSET + node.coord_y
|
||||
|
||||
sex = person.get_gender()
|
||||
if sex == Person.MALE:
|
||||
divclass = "male"
|
||||
elif sex == Person.FEMALE:
|
||||
@ -991,9 +995,8 @@ class PersonPages(BasePage):
|
||||
newpath = newpath.replace('\\', "/")
|
||||
thumbnail_url = newpath
|
||||
else:
|
||||
(photo_url,
|
||||
thumbnail_url) = self.report.prepare_copy_media(
|
||||
photo)
|
||||
(dummy_photo_url, thumbnail_url) = \
|
||||
self.report.prepare_copy_media(photo)
|
||||
thumbnail_url = "/".join(['..']*3 + [thumbnail_url])
|
||||
if win():
|
||||
thumbnail_url = thumbnail_url.replace('\\', "/")
|
||||
@ -1020,140 +1023,194 @@ class PersonPages(BasePage):
|
||||
|
||||
return [boxbg, shadow]
|
||||
|
||||
def extend_line(self, coord_y0, coord_x0):
|
||||
def extend_line(self, c_node, p_node):
|
||||
"""
|
||||
Draw and extended line
|
||||
Draw a line 'half the distance out to the parents. connect_line()
|
||||
will then draw the horizontal to the parent and the vertical connector
|
||||
to this line.
|
||||
|
||||
@param: coord_y0 -- The starting point
|
||||
@param: coord_x0 -- The end of the line
|
||||
@param c_node -- Child node to draw from
|
||||
@param p_node -- Parent node to draw towards
|
||||
"""
|
||||
width = (p_node.coord_x - c_node.coord_x - _WIDTH + 1)/2
|
||||
assert width > 0
|
||||
coord_x0 = _XOFFSET + c_node.coord_x + _WIDTH
|
||||
coord_y0 = c_node.coord_y + _LOFFSET + _VGAP/2
|
||||
|
||||
style = "top: %dpx; left: %dpx; width: %dpx"
|
||||
ext_bv = Html("div", class_="bvline", inline=True,
|
||||
style=style % (coord_y0, coord_x0, _HGAP/2)
|
||||
)
|
||||
ext_gv = Html("div", class_="gvline", inline=True,
|
||||
style=style % (coord_y0+_SHADOW,
|
||||
coord_x0, _HGAP/2+_SHADOW)
|
||||
)
|
||||
return [ext_bv, ext_gv]
|
||||
bvline = Html("div", class_="bvline", inline=True,
|
||||
style=style % (coord_y0, coord_x0, width))
|
||||
gvline = Html("div", class_="gvline", inline=True,
|
||||
style=style % (
|
||||
coord_y0+_SHADOW, coord_x0, width+_SHADOW))
|
||||
return [bvline, gvline]
|
||||
|
||||
def connect_line(self, coord_y0, coord_y1, col):
|
||||
def connect_line(self, coord_xc, coord_yc, coord_xp, coord_yp):
|
||||
"""
|
||||
We need to draw a line between to points
|
||||
Draw the line horizontally back from the parent towards the child and
|
||||
then the vertical connecting this line to the line drawn towards us
|
||||
from the child.
|
||||
|
||||
@param: coord_y0 -- The starting point
|
||||
@param: coord_y1 -- The end of the line
|
||||
@param: col -- The generation number
|
||||
@param: coord_cx -- X coordinate for the child
|
||||
@param: coord_yp -- Y coordinate for the child
|
||||
@param: coord_xp -- X coordinate for the parent
|
||||
@param: coord_yp -- Y coordinate for the parent
|
||||
"""
|
||||
coord_y = min(coord_y0, coord_y1)
|
||||
coord_y = min(coord_yc, coord_yp)
|
||||
|
||||
# xh is the X co-ordinate half way between the two nodes.
|
||||
# dx is the X gap between the two nodes, remembering that the
|
||||
# the coordinates are for the LEFT of both nodes.
|
||||
coord_xh = (coord_xp + _WIDTH + coord_xc)/2
|
||||
width_x = (coord_xp - _WIDTH - coord_xc)/2
|
||||
assert width_x >= 0
|
||||
stylew = "top: %dpx; left: %dpx; width: %dpx;"
|
||||
styleh = "top: %dpx; left: %dpx; height: %dpx;"
|
||||
coord_x0 = _XOFFSET + col * _WIDTH + (col-1)*_HGAP + _HGAP/2
|
||||
cnct_bv = Html("div", class_="bvline", inline=True,
|
||||
style=stylew % (coord_y1, coord_x0, _HGAP/2))
|
||||
style=stylew % (coord_yp, coord_xh, width_x))
|
||||
cnct_gv = Html("div", class_="gvline", inline=True,
|
||||
style=stylew % (coord_y1+_SHADOW,
|
||||
coord_x0+_SHADOW,
|
||||
_HGAP/2+_SHADOW))
|
||||
style=stylew % (coord_yp+_SHADOW,
|
||||
coord_xh+_SHADOW,
|
||||
width_x))
|
||||
# Experience says that line heights need to be 1 longer than we
|
||||
# expect. I suspect this is because HTML treats the lines as
|
||||
# 'number of pixels starting at...' so to create a line between
|
||||
# pixels 2 and 5 we need to light pixels 2, 3, 4, 5 - FOUR - and
|
||||
# not 5 - 2 = 3.
|
||||
cnct_bh = Html("div", class_="bhline", inline=True,
|
||||
style=styleh % (coord_y, coord_x0,
|
||||
abs(coord_y0-coord_y1)))
|
||||
style=styleh % (coord_y, coord_xh,
|
||||
abs(coord_yp-coord_yc)+1))
|
||||
cnct_gh = Html("div", class_="gvline", inline=True,
|
||||
style=styleh % (coord_y+_SHADOW,
|
||||
coord_x0+_SHADOW,
|
||||
abs(coord_y0-coord_y1)))
|
||||
coord_xh+_SHADOW,
|
||||
abs(coord_yp-coord_yc)+1))
|
||||
cnct_gv = ''
|
||||
cnct_gh = ''
|
||||
return [cnct_bv, cnct_gv, cnct_bh, cnct_gh]
|
||||
|
||||
def draw_connected_box(self, center1, center2, col, handle):
|
||||
def draw_connected_box(self, p_node, c_node, gen, person):
|
||||
"""
|
||||
Draws the connected box for Ancestor Tree on the Individual Page
|
||||
|
||||
@param: center1 -- The first box to connect
|
||||
@param: center2 -- The destination box to draw
|
||||
@param: col -- The generation number
|
||||
@param: handle -- The handle of the person to set in the new box
|
||||
@param: p_node -- Parent node to draw and connect from
|
||||
@param: c_node -- Child node to connect towards
|
||||
@param: gen -- Generation providing an HTML style hint
|
||||
@param: handle -- Parent node handle
|
||||
"""
|
||||
coord_cx = _XOFFSET + c_node.coord_x
|
||||
coord_cy = _YOFFSET + c_node.coord_y
|
||||
coord_px = _XOFFSET+p_node.coord_x
|
||||
coord_py = _YOFFSET+p_node.coord_y
|
||||
box = []
|
||||
if not handle:
|
||||
if person is None:
|
||||
return box
|
||||
person = self.r_db.get_person_from_handle(handle)
|
||||
box = self.draw_box(center2, col, person)
|
||||
box += self.connect_line(center1, center2, col)
|
||||
box = self.draw_box(p_node, gen, person)
|
||||
box += self.connect_line(
|
||||
coord_cx, coord_cy+_LOFFSET, coord_px, coord_py+_LOFFSET)
|
||||
return box
|
||||
|
||||
def create_layout_tree(self, p_handle, generations):
|
||||
"""
|
||||
Create a family subtree in a format that is suitable to pass to
|
||||
the Buchheim algorithm.
|
||||
|
||||
@param: p_handle -- Handle for person at root of this subtree
|
||||
@param: generation -- Generations left to add to tree.
|
||||
"""
|
||||
family_tree = None
|
||||
if generations:
|
||||
if p_handle:
|
||||
person = self.r_db.get_person_from_handle(p_handle)
|
||||
if person is None:
|
||||
return None
|
||||
family_handle = person.get_main_parents_family_handle()
|
||||
f_layout_tree = None
|
||||
m_layout_tree = None
|
||||
if family_handle:
|
||||
family = self.r_db.get_family_from_handle(family_handle)
|
||||
if family is not None:
|
||||
f_handle = family.get_father_handle()
|
||||
m_handle = family.get_mother_handle()
|
||||
f_layout_tree = self.create_layout_tree(
|
||||
f_handle, generations-1)
|
||||
m_layout_tree = self.create_layout_tree(
|
||||
m_handle, generations-1)
|
||||
|
||||
family_tree = LayoutTree(
|
||||
p_handle, f_layout_tree, m_layout_tree)
|
||||
return family_tree
|
||||
|
||||
def display_tree(self):
|
||||
"""
|
||||
Display the Ancestor Tree
|
||||
Display the Ancestor tree using a Buchheim tree.
|
||||
|
||||
Reference: Improving Walker's Algorithm to Run in Linear time
|
||||
Christoph Buccheim, Michael Junger, Sebastian Leipert
|
||||
|
||||
This is more complex than a simple binary tree but it results in a much
|
||||
more compact, but still sensible, layout which is especially good where
|
||||
the tree has gaps that would otherwise result in large blank areas.
|
||||
"""
|
||||
tree = []
|
||||
if not self.person.get_main_parents_family_handle():
|
||||
family_handle = self.person.get_main_parents_family_handle()
|
||||
if not family_handle:
|
||||
return None
|
||||
|
||||
generations = self.report.options['graphgens']
|
||||
max_in_col = 1 << (generations-1)
|
||||
max_size = _HEIGHT*max_in_col + _VGAP*(max_in_col+1)
|
||||
center = int(max_size/2)
|
||||
|
||||
# Begin by building a representation of the Ancestry tree that can be
|
||||
# fed to the Buchheim algorithm. Note that the algorithm doesn't care
|
||||
# who is the father and who is the mother.
|
||||
#
|
||||
# This routine is also about to go recursive!
|
||||
layout_tree = self.create_layout_tree(
|
||||
self.person.get_handle(), generations)
|
||||
|
||||
# We now apply the Buchheim algorith to this tree, and it assigns X
|
||||
# and Y positions to all elements in the tree.
|
||||
l_tree = buchheim(layout_tree, _WIDTH, _HGAP, _HEIGHT, _VGAP)
|
||||
|
||||
# We know the height in 'pixels' where every Ancestor will sit
|
||||
# precisely on an integer unit boundary.
|
||||
with Html("div", id="tree", class_="subsection") as tree:
|
||||
tree += Html("h4", self._('Ancestors'), inline=True)
|
||||
tree += Html("h4", _('Ancestors'), inline=True)
|
||||
with Html("div", id="treeContainer",
|
||||
style="width:%dpx; height:%dpx;" % (
|
||||
_XOFFSET+(generations)*_WIDTH+(generations-1)*_HGAP,
|
||||
max_size)
|
||||
l_tree.width + _XOFFSET + _WIDTH,
|
||||
l_tree.height + _HEIGHT + _VGAP)
|
||||
) as container:
|
||||
tree += container
|
||||
container += self.draw_tree(1, generations, max_size,
|
||||
0, center, self.person.handle)
|
||||
container += self.draw_tree(l_tree, 1, None)
|
||||
|
||||
return tree
|
||||
|
||||
def draw_tree(self, gen_nr, maxgen, max_size, old_center,
|
||||
new_center, person_handle):
|
||||
def draw_tree(self, l_node, gen_nr, c_node):
|
||||
"""
|
||||
Draws the Ancestor Tree
|
||||
|
||||
@param: l_node -- The tree node to draw
|
||||
@param: gen_nr -- The generation number to draw
|
||||
@param: maxgen -- The maximum number of generations to draw
|
||||
@param: max_size -- The maximum size of the drawing area
|
||||
@param: old_center -- The position of the old box
|
||||
@param: new_center -- The position of the new box
|
||||
@param: person_handle -- The handle of the person to draw
|
||||
@param: c_node -- Child node of this parent
|
||||
"""
|
||||
tree = []
|
||||
if gen_nr > maxgen:
|
||||
return tree
|
||||
gen_offset = int(max_size / pow(2, gen_nr+1))
|
||||
if person_handle:
|
||||
person = self.r_db.get_person_from_handle(person_handle)
|
||||
else:
|
||||
person = None
|
||||
if not person:
|
||||
return tree
|
||||
person = self.r_db.get_person_from_handle(l_node.handle())
|
||||
if person is None:
|
||||
return None
|
||||
|
||||
if gen_nr == 1:
|
||||
tree = self.draw_box(new_center, 0, person)
|
||||
tree = self.draw_box(l_node, 0, person)
|
||||
else:
|
||||
tree = self.draw_connected_box(old_center, new_center,
|
||||
gen_nr-1, person_handle)
|
||||
tree = self.draw_connected_box(
|
||||
l_node, c_node, gen_nr-1, person)
|
||||
|
||||
if gen_nr == maxgen:
|
||||
return tree
|
||||
# If there are any parents, we need to draw the extend line. We only
|
||||
# use the parent to define the end of the line so either will do and
|
||||
# we know we have at least one of this test passes.
|
||||
if l_node.children:
|
||||
tree += self.extend_line(l_node, l_node.children[0])
|
||||
|
||||
family_handle = person.get_main_parents_family_handle()
|
||||
if family_handle:
|
||||
line_offset = _XOFFSET + gen_nr*_WIDTH + (gen_nr-1)*_HGAP
|
||||
tree += self.extend_line(new_center, line_offset)
|
||||
# The parents are equivalent and the drawing routine figures out
|
||||
# whether they are male or female.
|
||||
for p_node in l_node.children:
|
||||
tree += self.draw_tree(p_node, gen_nr+1, l_node)
|
||||
|
||||
family = self.r_db.get_family_from_handle(family_handle)
|
||||
|
||||
f_center = new_center-gen_offset
|
||||
f_handle = family.get_father_handle()
|
||||
tree += self.draw_tree(gen_nr+1, maxgen, max_size,
|
||||
new_center, f_center, f_handle)
|
||||
|
||||
m_center = new_center+gen_offset
|
||||
m_handle = family.get_mother_handle()
|
||||
tree += self.draw_tree(gen_nr+1, maxgen, max_size,
|
||||
new_center, m_center, m_handle)
|
||||
return tree
|
||||
|
||||
def display_ind_associations(self, assoclist):
|
||||
@ -1237,7 +1294,8 @@ class PersonPages(BasePage):
|
||||
if birthorder:
|
||||
children = sorted(children)
|
||||
|
||||
for birthdate, birth, death, handle in children:
|
||||
for dummy_birthdate, dummy_birth, \
|
||||
dummy_death, handle in children:
|
||||
if handle == self.person.get_handle():
|
||||
child_ped(ol_html)
|
||||
elif handle:
|
||||
@ -1357,7 +1415,7 @@ class PersonPages(BasePage):
|
||||
tcell = Html("td", pname, class_="ColumnValue")
|
||||
# display any notes associated with this name
|
||||
notelist = name.get_note_list()
|
||||
if len(notelist):
|
||||
if notelist:
|
||||
unordered = Html("ul")
|
||||
|
||||
for notehandle in notelist:
|
||||
@ -1543,9 +1601,8 @@ class PersonPages(BasePage):
|
||||
child_ref.get_mother_relation())
|
||||
return (None, None)
|
||||
|
||||
def display_ind_parent_family(self, birthmother, birthfather, family,
|
||||
table,
|
||||
first=False):
|
||||
def display_ind_parent_family(
|
||||
self, birthmother, birthfather, family, table, first=False):
|
||||
"""
|
||||
Display the individual parent family
|
||||
|
||||
@ -1610,7 +1667,7 @@ class PersonPages(BasePage):
|
||||
# language but in the default language.
|
||||
# Does get_sibling_relationship_string work ?
|
||||
reln = reln[0].upper() + reln[1:]
|
||||
except:
|
||||
except Exception:
|
||||
reln = self._("Not siblings")
|
||||
|
||||
val1 = " "
|
||||
@ -1709,7 +1766,6 @@ class PersonPages(BasePage):
|
||||
Display step families
|
||||
|
||||
@param: parent_handle -- The family parent handle to display
|
||||
@param: family -- The family
|
||||
@param: all_family_handles -- All known family handles
|
||||
@param: birthmother -- The birth mother
|
||||
@param: birthfather -- The birth father
|
||||
@ -1724,6 +1780,7 @@ class PersonPages(BasePage):
|
||||
self.display_ind_parent_family(birthmother, birthfather,
|
||||
parent_family, table)
|
||||
all_family_handles.append(parent_family_handle)
|
||||
return
|
||||
|
||||
def display_ind_center_person(self):
|
||||
"""
|
||||
@ -1741,7 +1798,7 @@ class PersonPages(BasePage):
|
||||
center_person,
|
||||
self.person)
|
||||
if relationship == "": # No relation to display
|
||||
return
|
||||
return None
|
||||
|
||||
# begin center_person division
|
||||
section = ""
|
||||
|
@ -777,12 +777,14 @@ gramps/plugins/view/view.gpr.py
|
||||
gramps/plugins/webreport/addressbook.py
|
||||
gramps/plugins/webreport/addressbooklist.py
|
||||
gramps/plugins/webreport/basepage.py
|
||||
gramps/plugins/webreport/buchheim.py
|
||||
gramps/plugins/webreport/contact.py
|
||||
gramps/plugins/webreport/download.py
|
||||
gramps/plugins/webreport/event.py
|
||||
gramps/plugins/webreport/family.py
|
||||
gramps/plugins/webreport/home.py
|
||||
gramps/plugins/webreport/introduction.py
|
||||
gramps/plugins/webreport/layout.py
|
||||
gramps/plugins/webreport/media.py
|
||||
gramps/plugins/webreport/narrativeweb.py
|
||||
gramps/plugins/webreport/person.py
|
||||
|
BIN
test/AncestorTree/AncestorTree.gramps
Normal file
BIN
test/AncestorTree/AncestorTree.gramps
Normal file
Binary file not shown.
Loading…
Reference in New Issue
Block a user