diff --git a/gramps/plugins/webreport/buchheim.py b/gramps/plugins/webreport/buchheim.py new file mode 100644 index 000000000..b9d61d16a --- /dev/null +++ b/gramps/plugins/webreport/buchheim.py @@ -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 +# Copyright (C) 2007-2009 Gary Burton +# Copyright (C) 2007-2009 Stephane Charette +# Copyright (C) 2008-2009 Brian G. Matherly +# Copyright (C) 2008 Jason M. Simanek +# Copyright (C) 2008-2011 Rob G. Healey +# Copyright (C) 2010 Doug Blank +# 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 diff --git a/gramps/plugins/webreport/layout.py b/gramps/plugins/webreport/layout.py new file mode 100644 index 000000000..ba8698cc0 --- /dev/null +++ b/gramps/plugins/webreport/layout.py @@ -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 +# Copyright (C) 2007-2009 Gary Burton +# Copyright (C) 2007-2009 Stephane Charette +# Copyright (C) 2008-2009 Brian G. Matherly +# Copyright (C) 2008 Jason M. Simanek +# Copyright (C) 2008-2011 Rob G. Healey +# Copyright (C) 2010 Doug Blank +# 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) diff --git a/gramps/plugins/webreport/media.py b/gramps/plugins/webreport/media.py index ebdd1fc75..2addaaae5 100644 --- a/gramps/plugins/webreport/media.py +++ b/gramps/plugins/webreport/media.py @@ -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) diff --git a/gramps/plugins/webreport/narrativeweb.py b/gramps/plugins/webreport/narrativeweb.py index 77f57bff1..7e0870a3a 100644 --- a/gramps/plugins/webreport/narrativeweb.py +++ b/gramps/plugins/webreport/narrativeweb.py @@ -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) diff --git a/gramps/plugins/webreport/person.py b/gramps/plugins/webreport/person.py index f072b117c..25ac44a04 100644 --- a/gramps/plugins/webreport/person.py +++ b/gramps/plugins/webreport/person.py @@ -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 = "" diff --git a/po/POTFILES.in b/po/POTFILES.in index db1780f9f..8e7a557f5 100755 --- a/po/POTFILES.in +++ b/po/POTFILES.in @@ -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 diff --git a/test/AncestorTree/AncestorTree.gramps b/test/AncestorTree/AncestorTree.gramps new file mode 100644 index 000000000..de947de0d Binary files /dev/null and b/test/AncestorTree/AncestorTree.gramps differ