gramps/src/plugins/RelGraph.py
Don Allingham 75fe88c22d * src/plugins/rel_it.py: Italian relationship calculator
added
* src/plugins/RelGraph.py: Advanced relationship graph
report generator


svn: r2232
2003-10-11 22:59:32 +00:00

913 lines
36 KiB
Python

#
# Gramps - a GTK+/GNOME based genealogy program
#
# Copyright (C) 2000-2003 Donald N. Allingham
# Contributions by Lorenzo Cappelletti <lorenzo.cappelletti@email.it>
#
# 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 2>/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"
)