diff --git a/src/plugins/RelGraph.py b/src/plugins/RelGraph.py new file mode 100644 index 000000000..b6f7232d8 --- /dev/null +++ b/src/plugins/RelGraph.py @@ -0,0 +1,912 @@ +# +# Gramps - a GTK+/GNOME based genealogy program +# +# Copyright (C) 2000-2003 Donald N. Allingham +# Contributions by Lorenzo Cappelletti +# +# 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +# + +"Generate files/Relationship graph" + +#------------------------------------------------------------------------ +# +# python modules +# +#------------------------------------------------------------------------ +import os +import string + +from sets import Set +from time import asctime + +#------------------------------------------------------------------------ +# +# GNOME/gtk +# +#------------------------------------------------------------------------ +import gtk + +#------------------------------------------------------------------------ +# +# GRAMPS modules +# +#------------------------------------------------------------------------ +import Utils +import Report +import BaseDoc +import GenericFilter +import Errors + +from RelLib import Event +from gettext import gettext as _ +from latin_utf8 import utf8_to_latin + +#------------------------------------------------------------------------ +# +# constants +# +#------------------------------------------------------------------------ +_scaled = 0 +_single = 1 +_multiple = 2 + +_pagecount_map = { + _("Single (scaled)") : _scaled, + _("Single") : _single, + _("Multiple") : _multiple, + } + +#------------------------------------------------------------------------ +# +# RelGraphDialog +# +#------------------------------------------------------------------------ +class RelGraphDialog(Report.ReportDialog): + + # Default graph options + File = None + IndividualSet = Set() + ShowAsStack = 0 + ShowFamilies = 0 + IncludeDates = 1 + JustYear = 0 + PlaceCause = 1 + IncludeUrl = 1 + IncludeId = 0 + Colorize = 1 + FontStyle = 'Arial' + ArrowHeadStyle = 'none' + ArrowTailStyle = 'normal' + AdoptionsDashed = 1 + Width = 0 + Height = 0 + HPages = 1 + VPages = 1 + TBMargin = 0 + LRMargin = 0 + + def __init__(self,database,person): + Report.ReportDialog.__init__(self,database,person) + + def get_title(self): + """The window title for this dialog""" + return "%s - %s - GRAMPS" % (_("Relationship Graph"), + _("Graphical Reports")) + + def get_target_browser_title(self): + """The title of the window created when the 'browse' button is + clicked in the 'Save As' frame.""" + return _("Graphviz File") + + def get_print_pagecount_map(self): + """Set up the list of possible page counts.""" + return (_pagecount_map, _("Single (scaled)")) + + def get_report_generations(self): + """Default to 10 generations, no page breaks.""" + return (10, 0) + + def get_report_filters(self): + """Set up the list of possible content filters.""" + + name = self.person.getPrimaryName().getName() + + all = GenericFilter.GenericFilter() + all.set_name(_("Entire Database")) + all.add_rule(GenericFilter.Everyone([])) + + des = GenericFilter.GenericFilter() + des.set_name(_("Descendants of %s") % name) + des.add_rule(GenericFilter.IsDescendantOf([self.person.getId()])) + + fam = GenericFilter.GenericFilter() + fam.set_name(_("Descendant family members of %s") % name) + fam.add_rule(GenericFilter.IsDescendantFamilyOf([self.person.getId()])) + + ans = GenericFilter.GenericFilter() + ans.set_name(_("Ancestors of %s") % name) + ans.add_rule(GenericFilter.IsAncestorOf([self.person.getId()])) + + com = GenericFilter.GenericFilter() + com.set_name(_("People with common ancestor with %s") % name) + com.add_rule(GenericFilter.HasCommonAncestorWith([self.person.getId()])) + + return [all, des, fam, ans, com] + + def add_user_options(self): + self.arrowstyle_optionmenu = gtk.OptionMenu() + menu = gtk.Menu() + + menuitem = gtk.MenuItem(_("Descendants <- Ancestors")) + menuitem.set_data('t', ('none', 'normal')) + menuitem.show() + menu.append(menuitem) + + menuitem = gtk.MenuItem(_("Descendants -> Ancestors")) + menuitem.set_data('t', ('normal', 'none')) + menuitem.show() + menu.append(menuitem) + + menuitem = gtk.MenuItem(_("Descendants <-> Ancestors")) + menuitem.set_data('t', ('normal', 'normal')) + menuitem.show() + menu.append(menuitem) + + menuitem = gtk.MenuItem(_("Descendants - Ancestors")) + menuitem.set_data('t', ('none', 'none')) + menuitem.show() + menu.append(menuitem) + + menu.set_active(0) + + self.arrowstyle_optionmenu.set_menu(menu) + + self.font_optionmenu = gtk.OptionMenu() + menu = gtk.Menu() + + menuitem = gtk.MenuItem(_("TrueType")) + menuitem.set_data('t', 'Arial') + menuitem.show() + menu.append(menuitem) + + menuitem = gtk.MenuItem(_("PostScript")) + menuitem.set_data('t', 'Helvetica') + menuitem.show() + menu.append(menuitem) + + self.font_optionmenu.set_menu(menu) + + self.add_frame_option(_("GraphViz Options"), + _("Font Options"), + self.font_optionmenu, + _("Choose the font family.")) + + self.add_frame_option(_("GraphViz Options"), + _("Arrowhead Options"), + self.arrowstyle_optionmenu, + _("Choose the direction that the arrows point.")) + + + self.show_as_stack_cb = gtk.CheckButton(_("Show family as a stack")) + self.show_as_stack_cb.set_active(self.ShowAsStack) + self.show_as_stack_cb.connect('toggled', self._grey_out_cb) + self.add_frame_option(_("GraphViz Options"), '', + self.show_as_stack_cb, + _("The main individual is shown along with " + "their spuses in a stack.")) + + self.show_families_cb = gtk.CheckButton(_("Show family nodes")) + self.show_families_cb.set_active(self.ShowFamilies) + self.show_families_cb.connect('toggled', self._grey_out_cb) + self.add_frame_option(_("GraphViz Options"), '', + self.show_families_cb, + _("Families will show up as ellipses, linked " + "to parents and children.")) + msg = _("Include IDs") + self.includeid_cb = gtk.CheckButton(msg) + self.includeid_cb.set_active(self.IncludeId) + self.add_frame_option(_("GraphViz Options"), '', + self.includeid_cb, + _("Include individual and family IDs.")) + + msg = _("Include Birth, Marriage and Death Dates") + self.includedates_cb = gtk.CheckButton(msg) + self.includedates_cb.set_active(self.IncludeDates) + self.add_frame_option(_("GraphViz Options"), '', + self.includedates_cb, + _("Include the dates that the individual " + "was born, got married and/or died " + "in the graph labels.")) + + self.just_year_cb = gtk.CheckButton(_("Limit dates to years only")) + self.just_year_cb.set_active(self.JustYear) + self.add_frame_option(_("GraphViz Options"), '', + self.just_year_cb, + _("Prints just dates' year, neither " + "month or day nor date approximation " + "or interval are shown.")) + + self.place_cause_cb = gtk.CheckButton(_("Place/cause when no date")) + self.place_cause_cb.set_active(self.PlaceCause) + self.includedates_cb.connect('toggled', self._grey_out_cb) + self.add_frame_option(_("GraphViz Options"), '', + self.place_cause_cb, + _("When no birth, marriage, or death date " + "is available, the correspondent place field " + "(or cause field when blank) will be used.")) + + self.includeurl_cb = gtk.CheckButton(_("Include URLs")) + self.includeurl_cb.set_active(self.IncludeUrl) + self.add_frame_option(_("GraphViz Options"), '', + self.includeurl_cb, + _("Include a URL in each graph node so " + "that PDF and imagemap files can be " + "generated that contain active links " + "to the files generated by the 'Generate " + "Web Site' report.")) + + self.colorize_cb = gtk.CheckButton(_("Colorize Graph")) + self.colorize_cb.set_active(self.Colorize) + self.add_frame_option(_("GraphViz Options"), + '', + self.colorize_cb, + _("Males will be outlined in blue, females " + "will be outlined in pink. If the sex of " + "an individual is unknown it will be " + "outlined in black.")) + + self.adoptionsdashed_cb = gtk.CheckButton(_("Indicate non-birth relationships with dashed lines")) + self.adoptionsdashed_cb.set_active(self.AdoptionsDashed) + self.add_frame_option(_("GraphViz Options"), + '', + self.adoptionsdashed_cb, + _("Non-birth relationships will show up " + "as dashed lines in the graph.")) + + tb_margin_adj = gtk.Adjustment(value=0.5, lower=0.25, + upper=100.0, step_incr=0.25) + lr_margin_adj = gtk.Adjustment(value=0.5, lower=0.25, + upper=100.0, step_incr=0.25) + + self.tb_margin_sb = gtk.SpinButton(adjustment=tb_margin_adj, digits=2) + self.lr_margin_sb = gtk.SpinButton(adjustment=lr_margin_adj, digits=2) + + self.add_frame_option(_("Page Options"), + _("Top & Bottom Margins"), + self.tb_margin_sb) + self.add_frame_option(_("Page Options"), + _("Left & Right Margins"), + self.lr_margin_sb) + + hpages_adj = gtk.Adjustment(value=1, lower=1, upper=25, step_incr=1) + vpages_adj = gtk.Adjustment(value=1, lower=1, upper=25, step_incr=1) + + self.hpages_sb = gtk.SpinButton(adjustment=hpages_adj, digits=0) + self.vpages_sb = gtk.SpinButton(adjustment=vpages_adj, digits=0) + + self.add_frame_option(_("Page Options"), + _("Number of Horizontal Pages"), + self.hpages_sb, + _("GraphViz can create very large graphs by " + "spreading the graph across a rectangular " + "array of pages. This controls the number " + "pages in the array horizontally.")) + self.add_frame_option(_("Page Options"), + _("Number of Vertical Pages"), + self.vpages_sb, + _("GraphViz can create very large graphs " + "by spreading the graph across a " + "rectangular array of pages. This " + "controls the number pages in the array " + "vertically.")) + + def _grey_out_cb (self, button): + if button == self.includedates_cb: + if button.get_active(): + self.just_year_cb.set_sensitive(1) + self.place_cause_cb.set_sensitive(1) + else: + self.just_year_cb.set_sensitive(0) + self.place_cause_cb.set_sensitive(0) + elif button == self.show_families_cb: + if button.get_active(): + self.show_as_stack_cb.set_sensitive(0) + else: + self.show_as_stack_cb.set_sensitive(1) + elif button == self.show_as_stack_cb: + if button.get_active(): + self.show_families_cb.set_sensitive(0) + else: + self.show_families_cb.set_sensitive(1) + + def make_doc_menu(self): + """Build a one item menu of document types that are + appropriate for this report.""" + name = "Graphviz (dot)" + menuitem = gtk.MenuItem (name) + menuitem.set_data ("d", name) + menuitem.set_data("paper",1) + if os.system ("dot /dev/null") == 0: + menuitem.set_data ("printable", _("Generate print output")) + menuitem.show () + myMenu = gtk.Menu () + myMenu.append (menuitem) + self.format_menu.set_menu(myMenu) + + def make_document(self): + """Do Nothing. This document will be created in the + make_report routine.""" + pass + + def setup_style_frame(self): + """The style frame is not used in this dialog.""" + pass + + def parse_style_frame(self): + """The style frame is not used in this dialog.""" + pass + + def parse_other_frames(self): + self.ShowAsStack = self.show_as_stack_cb.get_active() + self.ShowFamilies = self.show_families_cb.get_active() + self.IncludeDates = self.includedates_cb.get_active() + self.JustYear = self.just_year_cb.get_active() + self.PlaceCause = self.place_cause_cb.get_active() + self.IncludeId = self.includeid_cb.get_active() + self.IncludeUrl = self.includeurl_cb.get_active() + self.Colorize = self.colorize_cb.get_active() + self.FontStyle =\ + self.font_optionmenu.get_menu().get_active().get_data('t') + self.ArrowHeadStyle, \ + self.ArrowTailStyle =\ + self.arrowstyle_optionmenu.get_menu().get_active().get_data('t') + self.AdoptionsDashed = self.adoptionsdashed_cb.get_active() + self.HPages = self.hpages_sb.get_value_as_int() + self.VPages = self.vpages_sb.get_value_as_int() + self.TBMargin = self.tb_margin_sb.get_value() + self.LRMargin = self.lr_margin_sb.get_value() + + def make_report(self): + """Create the object that will produce the GraphViz file.""" + self.Width = self.paper.get_width_inches() + self.Height = self.paper.get_height_inches() + + self.File = open(self.target_path,"w") + + try: + self.IndividualSet =\ + Set(self.filter.apply(self.db, self.db.getPersonMap().values())) + self.IndividualSet.add(self.person) + except Errors.FilterError, msg: + from QuestionDialog import ErrorDialog + (m1,m2) = msg.messages() + ErrorDialog(m1,m2) + + _writeDot(self) + + if self.print_report.get_active (): + os.environ["DOT"] = self.target_path + os.system ('dot -Tps "$DOT" | %s &' % + Report.get_print_dialog_app ()) + +#------------------------------------------------------------------------ +# +# +# +#------------------------------------------------------------------------ +def report(database,person): + RelGraphDialog(database,person) + +#------------------------------------------------------------------------ +# +# _writeDot +# +#------------------------------------------------------------------------ +def _writeDot(self): + """Write out to a file a relationship graph in dot format""" + self.File.write("/* GRAMPS - Relationship graph\n") + self.File.write(" *\n") + self.File.write(" * Report options:\n") + self.File.write(" * font style : %s\n" % self.FontStyle) + self.File.write(" * style arrow head : %s\n" % self.ArrowHeadStyle) + self.File.write(" * tail : %s\n" % self.ArrowTailStyle) + self.File.write(" * include URLs : %s\n" % self.IncludeUrl) + self.File.write(" * IDs : %s\n" % self.IncludeId) + self.File.write(" * dates : %s\n" % self.IncludeDates) + self.File.write(" * just year : %s\n" % self.JustYear) + self.File.write(" * place or cause : %s\n" % self.PlaceCause) + self.File.write(" * colorize : %s\n" % self.Colorize) + self.File.write(" * dashed adoptions : %s\n" % self.AdoptionsDashed) + self.File.write(" * show families : %s\n" % self.ShowFamilies) + self.File.write(" * as stack : %s\n" % self.ShowAsStack) + self.File.write(" * margins top/bottm : %s\n" % self.TBMargin) + self.File.write(" * left/right : %s\n" % self.LRMargin) + self.File.write(" * pages horizontal : %s\n" % self.HPages) + self.File.write(" * vertical : %s\n" % self.VPages) + self.File.write(" * page width : %sin\n" % self.Width) + self.File.write(" * height : %sin\n" % self.Height) + self.File.write(" *\n") + self.File.write(" * Generated on %s by GRAMPS\n" % asctime()) + self.File.write(" */\n\n") + self.File.write("digraph GRAMPS_relationship_graph {\n") + self.File.write("bgcolor=white;\n") + self.File.write("rankdir=LR;\n") + self.File.write("center=1;\n") + self.File.write("margin=0.5;\n") + self.File.write("ratio=fill;\n") + self.File.write("size=\"%3.1f,%3.1f\";\n" + % ((self.Width*self.HPages) - (self.LRMargin*2) - + ((self.HPages - 1)*1.0), + (self.Height*self.VPages) - (self.TBMargin*2) - + ((self.VPages - 1)*1.0))) + self.File.write("page=\"%3.1f,%3.1f\";\n" % (self.Width, self.Height)) + + if len(self.IndividualSet) > 1: + if self.ShowAsStack: + _writeGraphRecord(self) + else: + _writeGraphBox(self) + + self.File.write("}\n// File end") + self.File.close() + +#------------------------------------------------------------------------ +# +# _writeGraphBox +# +#------------------------------------------------------------------------ +def _writeGraphBox (self): + """Write out a graph body where all individuals are separated boxes""" + individualNodes = Set() # list of individual graph nodes + familyNodes = Set() # list of family graph nodes + # Writes out a not for each individual + self.File.write('\n// Individual nodes (box graph)\n') + _writeNode(self.File, shape='box', color='black', fontname=self.FontStyle) + for individual in self.IndividualSet: + individualNodes.add(individual) + individualId = _getIndividualId(individual) + (color, url) = _getIndividualData(self, individual) + label = _getIndividualLabel(self, individual) + _writeNode(self.File, individualId, label, color, url) + # Writes out a node for each family + if self.ShowFamilies: + self.File.write('\n// Family nodes (box graph)\n') + _writeNode(self.File, shape='ellipse', color='black', + fontname=self.FontStyle) + for individual in individualNodes: + for family in individual.getFamilyList(): + if family not in familyNodes: + familyNodes.add(family) + familyId = _getFamilyId(family) + label = _getFamilyLabel(self, family) + _writeNode(self.File, familyId, label) + # Links each individual to their parents/family + self.File.write('\n// Individual edges\n') + _writeEdge(self.File, style="solid", arrowHead=self.ArrowHeadStyle, + arrowTail=self.ArrowTailStyle) + for individual in individualNodes: + individualId = _getIndividualId(individual) + for family, motherRelShip, fatherRelShip\ + in individual.getParentList(): + father = family.getFather() + mother = family.getMother() + if self.ShowFamilies and family in familyNodes: + # edge from an individual to their family + familyId = _getFamilyId(family) + style = _getEdgeStyle(self, fatherRelShip, motherRelShip) + _writeEdge(self.File, individualId, familyId, style) + else: + # edge from an individual to their parents + if father and father in individualNodes: + fatherId = _getIndividualId(father) + _writeEdge(self.File, individualId, fatherId, + _getEdgeStyle(self, fatherRelShip)) + if mother and mother in individualNodes: + motherId = _getIndividualId(mother) + _writeEdge(self.File, individualId, motherId, + _getEdgeStyle(self, motherRelShip)) + # Links each family to its components + if self.ShowFamilies: + self.File.write('\n// Family edges (box graph)\n') + _writeEdge(self.File, style="solid", arrowHead=self.ArrowHeadStyle, + arrowTail=self.ArrowTailStyle) + for family in familyNodes: + familyId = _getFamilyId(family) + father = family.getFather() + if father and father in individualNodes: + fatherId = _getIndividualId(father) + _writeEdge(self.File, familyId, fatherId) + mother = family.getMother() + if mother and mother in individualNodes: + motherId = _getIndividualId(mother) + _writeEdge(self.File, familyId, motherId) + # Statistics + males = 0 + females = 0 + unknowns = 0 + for individual in individualNodes: + if individual.getGender() == individual.male: + males = males + 1 + elif individual.getGender() == individual.female: + females = females + 1 + else: + unknowns = unknowns + 1 + _writeStats(self.File, males, females, unknowns, len(familyNodes)) + +#------------------------------------------------------------------------ +# +# _writeGraphRecord +# +#------------------------------------------------------------------------ +def _writeGraphRecord (self): + """Write out a graph body where families are rendered as records""" + # Builds a dictionary of family records. + # Each record is made of an individual married with zero or + # more individuals. + familyNodes = {} + if isinstance(self.filter.get_rules()[0], + GenericFilter.IsDescendantFamilyOf): + # With the IsDescendantFamilyOf filter, the IndividualSet + # includes people which are not direct descendants of the + # active person (practically, spouses of direct + # discendants). Because we want the graph to highlight the + # consanguinity, IndividualSet is split in two subsets: + # naturalRelatives (direct descendants) and its complementary + # subset (in-law relatives). + filter = GenericFilter.GenericFilter() + filter.add_rule(GenericFilter.IsDescendantOf([self.person.getId()])) + naturalRelatives =\ + Set(filter.apply(self.db, self.db.getPersonMap().values())) + naturalRelatives.add(self.person) + else: + naturalRelatives = self.IndividualSet + self.File.write('\n// Family nodes (record graph)\n') + _writeNode(self.File, shape='record', color='black', + fontname=self.FontStyle) + for individual in naturalRelatives: + familyId = _getIndividualId(individual) + # If both husband and wife are members of the IndividualSet, + # only one record, with the husband first, is displayed. + if individual.getGender() == individual.female: + # There are exactly three cases where a female node is added: + family = None # no family + husbands = [] # filtered-in husbands (naturalRelatives) + unknownHusbands = 0 # filtered-out/unknown husbands + for family in individual.getFamilyList(): + husband = family.getFather() + if husband and husband in self.IndividualSet: + if husband not in naturalRelatives: + husbands.append(husband) + else: + unknownHusbands = 1 + if not family or len(husbands) or unknownHusbands: + familyNodes[familyId] = [individual] + husbands + else: + familyNodes[familyId] = [individual] + for family in individual.getFamilyList(): + wife = family.getMother() + if wife in self.IndividualSet: + familyNodes[familyId].append(wife) + # Writes out all family records + for familyId, family in familyNodes.items(): + (color, url) = _getIndividualData(self, familyNodes[familyId][0]) + label = _getFamilyRecordLabel(self, familyNodes[familyId]) + _writeNode(self.File, familyId, label, color, url) + # Links individual's record to their parents' record + # The arrow goes from the individual port of a family record + # to the parent port of the parent's family record. + self.File.write('\n// Individual edges\n') + _writeEdge(self.File, style="solid", arrowHead=self.ArrowHeadStyle, + arrowTail=self.ArrowTailStyle) + for familyFromId, familyFrom in familyNodes.items(): + for individualFrom in familyFrom: + individualFromId = _getIndividualId(individualFrom) + for family, motherRelShip, fatherRelShip\ + in individualFrom.getParentList(): + father = family.getFather() + mother = family.getMother() + # Things are complicated here because a parent may or + # or may not exist. + if father: + fatherId = _getIndividualId(father) + else: + fatherId = "" + if mother: + motherId = _getIndividualId(mother) + else: + motherId = "" + if familyNodes.has_key(fatherId): + if mother in familyNodes[fatherId]: + _writeEdge(self.File, familyFromId, fatherId, + _getEdgeStyle(self, motherRelShip), + portFrom=individualFromId, portTo=motherId) + else: + _writeEdge(self.File, familyFromId, fatherId, + _getEdgeStyle(self, fatherRelShip), + portFrom=individualFromId) + if familyNodes.has_key(motherId): + if father in familyNodes[motherId]: + _writeEdge(self.File, familyFromId, motherId, + _getEdgeStyle(self, fatherRelShip), + portFrom=individualFromId, portTo=fatherId) + else: + _writeEdge(self.File, familyFromId, motherId, + _getEdgeStyle(self, motherRelShip), + portFrom=individualFromId) + # Stats (unique individuals) + males = Set() + females = Set() + unknowns = Set() + marriages = 0 + for familyId, family in familyNodes.items(): + marriages = marriages + (len(family) - 1) + for individual in family: + if individual.getGender() == individual.male\ + and individual not in males: + males.add(individual) + elif individual.getGender() == individual.female\ + and individual not in females: + females.add(individual) + elif individual.getGender() == individual.unknown\ + and individual not in unknowns: + unknowns.add(individual) + _writeStats(self.File, len(males), len(females), len(unknowns), marriages) + +#------------------------------------------------------------------------ +# +# _getIndividualId +# +#------------------------------------------------------------------------ +def _getIndividualId (individual): + """Returns an individual id suitable for dot""" + return individual.getId() + +#------------------------------------------------------------------------ +# +# _getIndividualData +# +#------------------------------------------------------------------------ +def _getIndividualData (self, individual): + """Returns a tuple of individual data""" + # color + color = '' + if self.Colorize: + gender = individual.getGender() + if gender == individual.male: + color = 'dodgerblue4' + elif gender == individual.female: + color = 'deeppink' + # url + url = '' + if self.IncludeUrl: + url = "%s.html" % _getIndividualId(individual) + + return (color, url) + +#------------------------------------------------------------------------ +# +# _getEventLabel +# +#------------------------------------------------------------------------ +def _getEventLabel (self, event): + """Returns a formatted string of event data suitable for a label""" + if self.IncludeDates and event: + dateObj = event.getDateObj() + if dateObj.getYearValid(): + if self.JustYear: + return "%i" % dateObj.getYear() + else: + return dateObj.getDate() + elif self.PlaceCause: + if event.getPlaceName(): + return event.getPlaceName() + else: + return event.getCause() + return '' + +#------------------------------------------------------------------------ +# +# _getIndividualLabel +# +#------------------------------------------------------------------------ +def _getIndividualLabel (self, individual, marriageEvent=None, family=None): + """Returns a formatted string of individual data suitable for a label + + Returned string always includes individual's name and optionally + individual's birth and death dates, individual's marriage date, + individual's and family's IDs. + """ + # Get data ready + individualId = individual.getId() + name = individual.getPrimaryName().getName() + if self.IncludeDates: + birth = _getEventLabel(self, individual.getBirth()) + death = _getEventLabel(self, individual.getDeath()) + if marriageEvent != None: + familyId = family.getId() + marriage = _getEventLabel(self, marriageEvent) + # Id + if self.IncludeId: + if marriageEvent != None: + label = "%s (%s)\\n" % (familyId, individualId) + else: + label = "%s\\n" % individualId + else: + label = "" + # Marriage date + if self.IncludeDates and (marriageEvent != None and marriage): + label = label + "%s\\n" % marriage + # Individual's name + label = label + name + # Birth and death + if self.IncludeDates and (birth or death): + label = label + "\\n%s - %s" % (birth, death) + return label + +#------------------------------------------------------------------------ +# +# _getEdgeStyle +# +#------------------------------------------------------------------------ +def _getEdgeStyle (self, fatherRelShip, motherRelShip="Birth"): + """Returns a edge style that depends on the relationships with parents""" + if self.AdoptionsDashed and \ + (fatherRelShip != "Birth" or motherRelShip != "Birth"): + return "dashed" + +#------------------------------------------------------------------------ +# +# _getFamilyId +# +#------------------------------------------------------------------------ +def _getFamilyId (family): + """Returns a family id suitable for dot""" + return family.getId() + +#------------------------------------------------------------------------ +# +# _getFamilyLabel +# +#------------------------------------------------------------------------ +def _getFamilyLabel (self, family): + """Returns a formatted string of family data suitable for a label""" + marriage = _getEventLabel(self, family.getMarriage()) + if self.IncludeId: + return "%s\\n%s" % (family.getId(), marriage) + else: + return marriage + +#------------------------------------------------------------------------ +# +# _getFamilyRecordLabel +# +#------------------------------------------------------------------------ +def _getFamilyRecordLabel (self, record): + """Returns a formatted string of a family record suitable for a label""" + labels = [] + spouse = record[0] + for individual in record: + individualId = _getIndividualId(individual) + if spouse == individual: + label = _getIndividualLabel(self, individual) + else: + marriageEvent = Event() + for individualFamily in individual.getFamilyList(): + if individualFamily in spouse.getFamilyList(): + marriageEvent = individualFamily.getMarriage() + if not marriageEvent: + marriageEvent = Event() + break + label = _getIndividualLabel(self, individual, marriageEvent, + individualFamily) + label = string.replace(label, "|", r"\|") + label = string.replace(label, "<", r"\<") + label = string.replace(label, ">", r"\>") + labels.append("<%s>%s" % (individualId, label)) + return string.join(labels, "|") + +#------------------------------------------------------------------------ +# +# _writeNode +# +#------------------------------------------------------------------------ +def _writeNode (file, node="node", label="", color="", url="", shape="", + fontname=""): + """Writes out an individual node""" + file.write('%s [' % node) + if label: + file.write('label="%s" ' % + utf8_to_latin(string.replace(label, '"', r'\"'))) + if color: + file.write('color=%s ' % color) + if url: + file.write('URL="%s" ' % string.replace(url, '"', r'\"')) + if shape: + file.write('shape=%s ' % shape) + if fontname: + file.write('fontname="%s" ' % fontname) + file.write('];\n') + +#------------------------------------------------------------------------ +# +# _writeEdge +# +#------------------------------------------------------------------------ +def _writeEdge (file, nodeFrom="", nodeTo="", style="", + arrowHead="", arrowTail="", portFrom="", portTo=""): + """Writes out an edge""" + if nodeFrom and nodeTo: + if portFrom: + frm = nodeFrom + ":" + portFrom + else: + frm = nodeFrom + if portTo: + to = nodeTo + ":" + portTo + else: + to = nodeTo + file.write('%s -> %s [' % (frm, to)) + else: + file.write('edge [') # default edge + if style: + file.write('style=%s ' % style) + if arrowHead: + file.write('arrowhead=%s ' % arrowHead) + if arrowTail: + file.write('arrowtail=%s ' % arrowTail) + file.write('];\n') + +#------------------------------------------------------------------------ +# +# _writeStats +# +#------------------------------------------------------------------------ +def _writeStats (file, males, females, unknowns, marriages): + file.write('\n/* Statistics\n') + file.write(' * individuals male : % 4d\n' % males) + file.write(' * female : % 4d\n' % females) + file.write(' * unknown : % 4d\n' % unknowns) + file.write(' * total : % 4d\n' % (males+females+unknowns)) + file.write(' * marriages : % 4d\n' % marriages) + file.write(' */\n') + +#------------------------------------------------------------------------ +# +# +# +#------------------------------------------------------------------------ +def get_description(): + return _("Generates relationship graphs, currently only in GraphViz " + "format. GraphViz (dot) can transform the graph into " + "postscript, jpeg, png, vrml, svg, and many other formats. " + "For more information or to get a copy of GraphViz, " + "goto http://www.graphviz.org") + +#------------------------------------------------------------------------ +# +# +# +#------------------------------------------------------------------------ +from Plugins import register_report + +register_report( + report, + _("Relationship Graph"), + status=(_("Beta")), + category=_("Graphical Reports"), + description=get_description(), + author_name="Donald N. Allingham", + author_email="dallingham@users.sourceforge.net" + )