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:
Paul D.Smith 2018-02-16 13:40:16 +00:00 committed by Nick Hall
parent acfbb0a763
commit 03a89c73e3
7 changed files with 583 additions and 134 deletions

View 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

View 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)

View File

@ -160,6 +160,27 @@ class MediaPages(BasePage):
idx = 1 idx = 1
total_m = len(sorted_media_handles) total_m = len(sorted_media_handles)
prev = sorted_media_handles[total_m-1] if total_m > 0 else 0 prev = sorted_media_handles[total_m-1] if total_m > 0 else 0
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
elif index < total:
next_ = sorted_media_handles[index]
elif len(self.unused_media_handles) > 0:
next_ = self.unused_media_handles[0]
else:
next_ = None
self.mediapage(self.report, title,
handle, (prev, next_, index, media_count))
prev = handle
step()
index += 1
total = len(self.unused_media_handles)
idx = 1
prev = sorted_media_handles[len(sorted_media_handles)-1]
if total > 0: if total > 0:
for media_handle in self.unused_media_handles: for media_handle in self.unused_media_handles:
media = self.r_db.get_media_from_handle(media_handle) media = self.r_db.get_media_from_handle(media_handle)

View File

@ -289,10 +289,11 @@ class NavWebReport(Report):
os.mkdir(dir_name) os.mkdir(dir_name)
except IOError as value: except IOError as value:
msg = _("Could not create the directory: %s" msg = _("Could not create the directory: %s"
) % dir_name + "\n" + value[1] ) % dir_name + "\n" + value.strerror
self.user.notify_error(msg) self.user.notify_error(msg)
return return
except: except Exception as exception:
LOG.exception(exception)
msg = _("Could not create the directory: %s") % dir_name msg = _("Could not create the directory: %s") % dir_name
self.user.notify_error(msg) self.user.notify_error(msg)
return return
@ -307,12 +308,13 @@ class NavWebReport(Report):
os.mkdir(image_dir_name) os.mkdir(image_dir_name)
except IOError as value: except IOError as value:
msg = _("Could not create the directory: %s" msg = _("Could not create the directory: %s"
) % image_dir_name + "\n" + value[1] ) % image_dir_name + "\n" + value.strerror
self.user.notify_error(msg) self.user.notify_error(msg)
return return
except: except Exception as exception:
LOG.exception(exception)
msg = _("Could not create the directory: %s" msg = _("Could not create the directory: %s"
) % image_dir_name + "\n" + value[1] ) % image_dir_name + "\n" + str(exception)
self.user.notify_error(msg) self.user.notify_error(msg)
return return
else: else:
@ -453,7 +455,7 @@ class NavWebReport(Report):
if self.archive: if self.archive:
self.archive.close() self.archive.close()
if len(_WRONGMEDIAPATH) > 0: if _WRONGMEDIAPATH:
error = '\n'.join([ error = '\n'.join([
_('ID=%(grampsid)s, path=%(dir)s') % { _('ID=%(grampsid)s, path=%(dir)s') % {
'grampsid' : x[0], 'grampsid' : x[0],
@ -856,7 +858,7 @@ class NavWebReport(Report):
@param: bkref_class -- The class associated to this handle (source) @param: bkref_class -- The class associated to this handle (source)
@param: bkref_handle -- The handle associated to this 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]: for bkref in self.bkref_dict[Source][source_handle]:
if bkref_handle == bkref[1]: if bkref_handle == bkref[1]:
return return
@ -893,7 +895,7 @@ class NavWebReport(Report):
@param: bkref_class -- The class associated to this handle @param: bkref_class -- The class associated to this handle
@param: bkref_handle -- The handle associated to this citation @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]: for bkref in self.bkref_dict[Citation][citation_handle]:
if bkref_handle == bkref[1]: if bkref_handle == bkref[1]:
return return
@ -926,7 +928,7 @@ class NavWebReport(Report):
@param: bkref_class -- The class associated to this handle (media) @param: bkref_class -- The class associated to this handle (media)
@param: bkref_handle -- The handle associated to this 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]: for bkref in self.bkref_dict[Media][media_handle]:
if bkref_handle == bkref[1]: if bkref_handle == bkref[1]:
return return
@ -967,7 +969,7 @@ class NavWebReport(Report):
@param: bkref_class -- The class associated to this handle (source) @param: bkref_class -- The class associated to this handle (source)
@param: bkref_handle -- The handle associated to this 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]: for bkref in self.bkref_dict[Repository][repos_handle]:
if bkref_handle == bkref[1]: if bkref_handle == bkref[1]:
return return
@ -1049,7 +1051,7 @@ class NavWebReport(Report):
# copy all to images subdir: # copy all to images subdir:
for from_path in imgs: 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") self.copy_file(from_path, fname, "images")
# copy Gramps marker icon for openstreetmap # copy Gramps marker icon for openstreetmap
@ -1126,12 +1128,10 @@ class NavWebReport(Report):
len(local_list)) as step: len(local_list)) as step:
SurnameListPage(self, self.title, ind_list, SurnameListPage(self, self.title, ind_list,
SurnameListPage.ORDER_BY_NAME, SurnameListPage.ORDER_BY_NAME, self.surname_fname)
self.surname_fname)
SurnameListPage(self, self.title, ind_list, SurnameListPage(self, self.title, ind_list,
SurnameListPage.ORDER_BY_COUNT, SurnameListPage.ORDER_BY_COUNT, "surnames_count")
"surnames_count")
index = 1 index = 1
for (surname, handle_list) in local_list: for (surname, handle_list) in local_list:
@ -1493,7 +1493,8 @@ class NavWebReport(Report):
try: try:
shutil.copyfile(from_fname, dest) shutil.copyfile(from_fname, dest)
os.utime(dest, (mtime, mtime)) os.utime(dest, (mtime, mtime))
except: except Exception as exception:
LOG.exception(exception)
print("Copying error: %s" % sys.exc_info()[1]) print("Copying error: %s" % sys.exc_info()[1])
print("Continuing...") print("Continuing...")
elif self.warn_dir: elif self.warn_dir:
@ -1666,8 +1667,9 @@ class NavWebOptions(MenuReportOptions):
cright.set_help(_("The copyright to be used for the web files")) cright.set_help(_("The copyright to be used for the web files"))
addopt("cright", cright) addopt("cright", cright)
self.__css = EnumeratedListOption(_('StyleSheet'), CSS["default"]["id"]) self.__css = EnumeratedListOption(('StyleSheet'), CSS["default"]["id"])
for (fname, gid) in sorted([(CSS[key]["translation"], CSS[key]["id"]) for (dummy_fname, gid) in sorted(
[(CSS[key]["translation"], CSS[key]["id"])
for key in list(CSS.keys())]): for key in list(CSS.keys())]):
if CSS[gid]["user"]: if CSS[gid]["user"]:
self.__css.add_item(CSS[gid]["id"], CSS[gid]["translation"]) self.__css.add_item(CSS[gid]["id"], CSS[gid]["translation"])
@ -1710,10 +1712,11 @@ class NavWebOptions(MenuReportOptions):
addopt("ancestortree", self.__ancestortree) addopt("ancestortree", self.__ancestortree)
self.__ancestortree.connect('value-changed', self.__graph_changed) 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 " self.__graphgens.set_help(_("The number of generations to include in "
"the ancestor graph")) "the ancestor graph"))
addopt("graphgens", self.__graphgens) addopt("graphgens", self.__graphgens)
self.__graph_changed() self.__graph_changed()
self.__securesite = BooleanOption(_("This is a secure site (https)"), self.__securesite = BooleanOption(_("This is a secure site (https)"),

View File

@ -76,6 +76,8 @@ from gramps.plugins.webreport.common import (get_first_letters, _KEYPERSON,
MARKER_PATH, OSM_MARKERS, MARKER_PATH, OSM_MARKERS,
GOOGLE_MAPS, MARKERS, html_escape, GOOGLE_MAPS, MARKERS, html_escape,
DROPMASTERS, FAMILYLINKS) DROPMASTERS, FAMILYLINKS)
from gramps.plugins.webreport.layout import LayoutTree
from gramps.plugins.webreport.buchheim import buchheim
_ = glocale.translation.sgettext _ = glocale.translation.sgettext
LOG = logging.getLogger(".NarrativeWeb") LOG = logging.getLogger(".NarrativeWeb")
@ -87,6 +89,8 @@ _VGAP = 10
_HGAP = 30 _HGAP = 30
_SHADOW = 5 _SHADOW = 5
_XOFFSET = 5 _XOFFSET = 5
_YOFFSET = 5
_LOFFSET = 20
################################################# #################################################
# #
@ -171,7 +175,7 @@ class PersonPages(BasePage):
showparents = report.options['showparents'] showparents = report.options['showparents']
output_file, sio = self.report.create_file("individuals") 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 date = 0
# begin Individuals division # begin Individuals division
@ -330,7 +334,7 @@ class PersonPages(BasePage):
family_list = person.get_family_handle_list() family_list = person.get_family_handle_list()
first_family = True first_family = True
#partner_name = None #partner_name = None
tcell = () # pylint: disable=R0204 tcell = ()
if family_list: if family_list:
for family_handle in family_list: for family_handle in family_list:
family = self.r_db.get_family_from_handle( family = self.r_db.get_family_from_handle(
@ -568,7 +572,7 @@ class PersonPages(BasePage):
individualdetail += self.display_ind_associations(assocs) individualdetail += self.display_ind_associations(assocs)
# for use in family map pages... # for use in family map pages...
if len(place_lat_long) > 0: if place_lat_long:
if self.report.options["familymappages"]: if self.report.options["familymappages"]:
# save output_file, string_io and cur_fname # save output_file, string_io and cur_fname
# before creating a new page # before creating a new page
@ -625,7 +629,7 @@ class PersonPages(BasePage):
minx, maxx = Decimal("0.00000001"), Decimal("0.00000001") minx, maxx = Decimal("0.00000001"), Decimal("0.00000001")
miny, maxy = Decimal("0.00000001"), Decimal("0.00000001") miny, maxy = Decimal("0.00000001"), Decimal("0.00000001")
xwidth, yheight = [], [] xwidth, yheight = [], []
midx_, midy_, spanx, spany = [None]*4 midx_, midy_, dummy_spanx, spany = [None]*4
number_markers = len(place_lat_long) number_markers = len(place_lat_long)
if number_markers > 1: if number_markers > 1:
@ -649,7 +653,7 @@ class PersonPages(BasePage):
midx_, midy_ = conv_lat_lon(midx_, midy_, "D.D8") midx_, midy_ = conv_lat_lon(midx_, midy_, "D.D8")
# get the integer span of latitude and longitude # get the integer span of latitude and longitude
spanx = int(maxx - minx) dummy_spanx = int(maxx - minx)
spany = int(maxy - miny) spany = int(maxy - miny)
# set zoom level based on span of Longitude? # set zoom level based on span of Longitude?
@ -940,17 +944,17 @@ class PersonPages(BasePage):
# return family map link to its caller # return family map link to its caller
return familymap 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... draw the box around the AncestorTree Individual name box...
@param: node -- The node defining the box location
@param: center -- The center of the box
@param: col -- The generation number @param: col -- The generation number
@param: person -- The person to set in the box @param: person -- The person to set in the box
""" """
top = center - _HEIGHT/2 xoff = _XOFFSET + node.coord_x
xoff = _XOFFSET+col*(_WIDTH+_HGAP) top = _YOFFSET + node.coord_y
sex = person.gender
sex = person.get_gender()
if sex == Person.MALE: if sex == Person.MALE:
divclass = "male" divclass = "male"
elif sex == Person.FEMALE: elif sex == Person.FEMALE:
@ -991,9 +995,8 @@ class PersonPages(BasePage):
newpath = newpath.replace('\\', "/") newpath = newpath.replace('\\', "/")
thumbnail_url = newpath thumbnail_url = newpath
else: else:
(photo_url, (dummy_photo_url, thumbnail_url) = \
thumbnail_url) = self.report.prepare_copy_media( self.report.prepare_copy_media(photo)
photo)
thumbnail_url = "/".join(['..']*3 + [thumbnail_url]) thumbnail_url = "/".join(['..']*3 + [thumbnail_url])
if win(): if win():
thumbnail_url = thumbnail_url.replace('\\', "/") thumbnail_url = thumbnail_url.replace('\\', "/")
@ -1020,140 +1023,194 @@ class PersonPages(BasePage):
return [boxbg, shadow] 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 c_node -- Child node to draw from
@param: coord_x0 -- The end of the line @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" style = "top: %dpx; left: %dpx; width: %dpx"
ext_bv = Html("div", class_="bvline", inline=True, bvline = Html("div", class_="bvline", inline=True,
style=style % (coord_y0, coord_x0, _HGAP/2) style=style % (coord_y0, coord_x0, width))
) gvline = Html("div", class_="gvline", inline=True,
ext_gv = Html("div", class_="gvline", inline=True, style=style % (
style=style % (coord_y0+_SHADOW, coord_y0+_SHADOW, coord_x0, width+_SHADOW))
coord_x0, _HGAP/2+_SHADOW) return [bvline, gvline]
)
return [ext_bv, ext_gv]
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_cx -- X coordinate for the child
@param: coord_y1 -- The end of the line @param: coord_yp -- Y coordinate for the child
@param: col -- The generation number @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;" stylew = "top: %dpx; left: %dpx; width: %dpx;"
styleh = "top: %dpx; left: %dpx; height: %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, 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, cnct_gv = Html("div", class_="gvline", inline=True,
style=stylew % (coord_y1+_SHADOW, style=stylew % (coord_yp+_SHADOW,
coord_x0+_SHADOW, coord_xh+_SHADOW,
_HGAP/2+_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, cnct_bh = Html("div", class_="bhline", inline=True,
style=styleh % (coord_y, coord_x0, style=styleh % (coord_y, coord_xh,
abs(coord_y0-coord_y1))) abs(coord_yp-coord_yc)+1))
cnct_gh = Html("div", class_="gvline", inline=True, cnct_gh = Html("div", class_="gvline", inline=True,
style=styleh % (coord_y+_SHADOW, style=styleh % (coord_y+_SHADOW,
coord_x0+_SHADOW, coord_xh+_SHADOW,
abs(coord_y0-coord_y1))) abs(coord_yp-coord_yc)+1))
cnct_gv = ''
cnct_gh = ''
return [cnct_bv, cnct_gv, cnct_bh, 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: p_node -- Parent node to draw and connect from
@param: c_node -- Child node to connect towards
@param: center1 -- The first box to connect @param: gen -- Generation providing an HTML style hint
@param: center2 -- The destination box to draw @param: handle -- Parent node handle
@param: col -- The generation number
@param: handle -- The handle of the person to set in the new box
""" """
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 = [] box = []
if not handle: if person is None:
return box return box
person = self.r_db.get_person_from_handle(handle) box = self.draw_box(p_node, gen, person)
box = self.draw_box(center2, col, person) box += self.connect_line(
box += self.connect_line(center1, center2, col) coord_cx, coord_cy+_LOFFSET, coord_px, coord_py+_LOFFSET)
return box 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): 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 = [] family_handle = self.person.get_main_parents_family_handle()
if not self.person.get_main_parents_family_handle(): if not family_handle:
return None return None
generations = self.report.options['graphgens'] 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: 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", with Html("div", id="treeContainer",
style="width:%dpx; height:%dpx;" % ( style="width:%dpx; height:%dpx;" % (
_XOFFSET+(generations)*_WIDTH+(generations-1)*_HGAP, l_tree.width + _XOFFSET + _WIDTH,
max_size) l_tree.height + _HEIGHT + _VGAP)
) as container: ) as container:
tree += container tree += container
container += self.draw_tree(1, generations, max_size, container += self.draw_tree(l_tree, 1, None)
0, center, self.person.handle)
return tree return tree
def draw_tree(self, gen_nr, maxgen, max_size, old_center, def draw_tree(self, l_node, gen_nr, c_node):
new_center, person_handle):
""" """
Draws the Ancestor Tree Draws the Ancestor Tree
@param: l_node -- The tree node to draw
@param: gen_nr -- The generation number to draw @param: gen_nr -- The generation number to draw
@param: maxgen -- The maximum number of generations to draw @param: c_node -- Child node of this parent
@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
""" """
tree = [] tree = []
if gen_nr > maxgen: person = self.r_db.get_person_from_handle(l_node.handle())
return tree if person is None:
gen_offset = int(max_size / pow(2, gen_nr+1)) return None
if person_handle:
person = self.r_db.get_person_from_handle(person_handle)
else:
person = None
if not person:
return tree
if gen_nr == 1: if gen_nr == 1:
tree = self.draw_box(new_center, 0, person) tree = self.draw_box(l_node, 0, person)
else: else:
tree = self.draw_connected_box(old_center, new_center, tree = self.draw_connected_box(
gen_nr-1, person_handle) l_node, c_node, gen_nr-1, person)
if gen_nr == maxgen: # If there are any parents, we need to draw the extend line. We only
return tree # 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() # The parents are equivalent and the drawing routine figures out
if family_handle: # whether they are male or female.
line_offset = _XOFFSET + gen_nr*_WIDTH + (gen_nr-1)*_HGAP for p_node in l_node.children:
tree += self.extend_line(new_center, line_offset) 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 return tree
def display_ind_associations(self, assoclist): def display_ind_associations(self, assoclist):
@ -1237,7 +1294,8 @@ class PersonPages(BasePage):
if birthorder: if birthorder:
children = sorted(children) 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(): if handle == self.person.get_handle():
child_ped(ol_html) child_ped(ol_html)
elif handle: elif handle:
@ -1357,7 +1415,7 @@ class PersonPages(BasePage):
tcell = Html("td", pname, class_="ColumnValue") tcell = Html("td", pname, class_="ColumnValue")
# display any notes associated with this name # display any notes associated with this name
notelist = name.get_note_list() notelist = name.get_note_list()
if len(notelist): if notelist:
unordered = Html("ul") unordered = Html("ul")
for notehandle in notelist: for notehandle in notelist:
@ -1543,9 +1601,8 @@ class PersonPages(BasePage):
child_ref.get_mother_relation()) child_ref.get_mother_relation())
return (None, None) return (None, None)
def display_ind_parent_family(self, birthmother, birthfather, family, def display_ind_parent_family(
table, self, birthmother, birthfather, family, table, first=False):
first=False):
""" """
Display the individual parent family Display the individual parent family
@ -1610,7 +1667,7 @@ class PersonPages(BasePage):
# language but in the default language. # language but in the default language.
# Does get_sibling_relationship_string work ? # Does get_sibling_relationship_string work ?
reln = reln[0].upper() + reln[1:] reln = reln[0].upper() + reln[1:]
except: except Exception:
reln = self._("Not siblings") reln = self._("Not siblings")
val1 = "&nbsp;&nbsp;&nbsp;&nbsp;" val1 = "&nbsp;&nbsp;&nbsp;&nbsp;"
@ -1709,7 +1766,6 @@ class PersonPages(BasePage):
Display step families Display step families
@param: parent_handle -- The family parent handle to display @param: parent_handle -- The family parent handle to display
@param: family -- The family
@param: all_family_handles -- All known family handles @param: all_family_handles -- All known family handles
@param: birthmother -- The birth mother @param: birthmother -- The birth mother
@param: birthfather -- The birth father @param: birthfather -- The birth father
@ -1724,6 +1780,7 @@ class PersonPages(BasePage):
self.display_ind_parent_family(birthmother, birthfather, self.display_ind_parent_family(birthmother, birthfather,
parent_family, table) parent_family, table)
all_family_handles.append(parent_family_handle) all_family_handles.append(parent_family_handle)
return
def display_ind_center_person(self): def display_ind_center_person(self):
""" """
@ -1741,7 +1798,7 @@ class PersonPages(BasePage):
center_person, center_person,
self.person) self.person)
if relationship == "": # No relation to display if relationship == "": # No relation to display
return return None
# begin center_person division # begin center_person division
section = "" section = ""

View File

@ -777,12 +777,14 @@ gramps/plugins/view/view.gpr.py
gramps/plugins/webreport/addressbook.py gramps/plugins/webreport/addressbook.py
gramps/plugins/webreport/addressbooklist.py gramps/plugins/webreport/addressbooklist.py
gramps/plugins/webreport/basepage.py gramps/plugins/webreport/basepage.py
gramps/plugins/webreport/buchheim.py
gramps/plugins/webreport/contact.py gramps/plugins/webreport/contact.py
gramps/plugins/webreport/download.py gramps/plugins/webreport/download.py
gramps/plugins/webreport/event.py gramps/plugins/webreport/event.py
gramps/plugins/webreport/family.py gramps/plugins/webreport/family.py
gramps/plugins/webreport/home.py gramps/plugins/webreport/home.py
gramps/plugins/webreport/introduction.py gramps/plugins/webreport/introduction.py
gramps/plugins/webreport/layout.py
gramps/plugins/webreport/media.py gramps/plugins/webreport/media.py
gramps/plugins/webreport/narrativeweb.py gramps/plugins/webreport/narrativeweb.py
gramps/plugins/webreport/person.py gramps/plugins/webreport/person.py

Binary file not shown.