ea3423f181
svn: r1583
458 lines
18 KiB
Python
458 lines
18 KiB
Python
#
|
|
# Gramps - a GTK+/GNOME based genealogy program
|
|
#
|
|
# Copyright (C) 2000 Donald N. Allingham
|
|
#
|
|
# 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
|
|
|
|
#------------------------------------------------------------------------
|
|
#
|
|
# GNOME/gtk
|
|
#
|
|
#------------------------------------------------------------------------
|
|
import gtk
|
|
|
|
#------------------------------------------------------------------------
|
|
#
|
|
# GRAMPS modules
|
|
#
|
|
#------------------------------------------------------------------------
|
|
import Utils
|
|
import Report
|
|
import TextDoc
|
|
import GenericFilter
|
|
from intl 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,
|
|
}
|
|
|
|
#------------------------------------------------------------------------
|
|
#
|
|
# GraphVizDialog
|
|
#
|
|
#------------------------------------------------------------------------
|
|
class GraphVizDialog(Report.ReportDialog):
|
|
|
|
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()]))
|
|
|
|
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,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.add_frame_option(_("GraphViz Options"),
|
|
_("Arrowhead Options"),
|
|
self.arrowstyle_optionmenu,
|
|
_("Choose the direction that the arrows point."))
|
|
|
|
msg = _("Include Birth and Death Dates")
|
|
self.includedates_cb = gtk.CheckButton(msg)
|
|
self.includedates_cb.set_active(1)
|
|
self.add_frame_option(_("GraphViz Options"), '',
|
|
self.includedates_cb,
|
|
_("Include the years that the individual "
|
|
"was born and/or died in the graph node "
|
|
"labels."))
|
|
|
|
self.includeurl_cb = gtk.CheckButton(_("Include URLs"))
|
|
self.includeurl_cb.set_active(1)
|
|
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(1)
|
|
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(1)
|
|
self.add_frame_option(_("GraphViz Options"),
|
|
'',
|
|
self.adoptionsdashed_cb,
|
|
_("Non-birth relationships will show up "
|
|
"as dashed lines in the graph."))
|
|
|
|
self.show_families_cb = gtk.CheckButton(_("Show family nodes"))
|
|
self.show_families_cb.set_active(0)
|
|
self.add_frame_option(_("GraphViz Options"),
|
|
'',
|
|
self.show_families_cb,
|
|
_("Families will show up as circles, linked "
|
|
"to parents and children."))
|
|
|
|
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(_("GraphViz Options"),
|
|
_("Top & Bottom Margins"),
|
|
self.tb_margin_sb)
|
|
self.add_frame_option(_("GraphViz 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(_("GraphViz 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(_("GraphViz 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 make_doc_menu(self):
|
|
"""Build a one item menu of document types that are
|
|
appropriate for this report."""
|
|
map = {"Graphviz (dot)" : None}
|
|
myMenu = Utils.build_string_optmenu(map, None)
|
|
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):
|
|
menu = self.arrowstyle_optionmenu.get_menu()
|
|
self.arrowheadstyle, self.arrowtailstyle = menu.get_active().get_data('t')
|
|
self.includedates = self.includedates_cb.get_active()
|
|
self.includeurl = self.includeurl_cb.get_active()
|
|
self.tb_margin = self.tb_margin_sb.get_value()
|
|
self.lr_margin = self.lr_margin_sb.get_value()
|
|
self.colorize = self.colorize_cb.get_active()
|
|
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.show_families = self.show_families_cb.get_active()
|
|
|
|
def make_report(self):
|
|
"""Create the object that will produce the GraphViz file."""
|
|
width = self.paper.get_width_inches()
|
|
height = self.paper.get_height_inches()
|
|
|
|
file = open(self.target_path,"w")
|
|
|
|
try:
|
|
ind_list = self.filter.apply(self.db, self.db.getPersonMap().values())
|
|
except Errors.FilterError, msg:
|
|
(m1,m2) = msg.messages()
|
|
ErrorDialog(m1,m2)
|
|
|
|
write_dot(file, ind_list, self.orien, width, height,
|
|
self.tb_margin, self.lr_margin, self.hpages,
|
|
self.vpages, self.includedates, self.includeurl,
|
|
self.colorize, self.adoptionsdashed, self.arrowheadstyle,
|
|
self.arrowtailstyle, self.show_families)
|
|
|
|
#------------------------------------------------------------------------
|
|
#
|
|
#
|
|
#
|
|
#------------------------------------------------------------------------
|
|
def report(database,person):
|
|
GraphVizDialog(database,person)
|
|
|
|
#------------------------------------------------------------------------
|
|
#
|
|
#
|
|
#
|
|
#------------------------------------------------------------------------
|
|
def write_dot(file, ind_list, orien, width, height, tb_margin,
|
|
lr_margin, hpages, vpages, includedates, includeurl,
|
|
colorize, adoptionsdashed, arrowheadstyle, arrowtailstyle,
|
|
show_families):
|
|
file.write("digraph g {\n")
|
|
file.write("bgcolor=white;\n")
|
|
file.write("rankdir=LR;\n")
|
|
file.write("center=1;\n")
|
|
file.write("margin=0.5;\n")
|
|
file.write("ratio=fill;\n")
|
|
file.write("size=\"%3.1f,%3.1f\";\n" % ((width*hpages)-(lr_margin*2)-((hpages-1)*1.0),
|
|
(height*vpages)-(tb_margin*2)-((vpages-1)*1.0)))
|
|
file.write("page=\"%3.1f,%3.1f\";\n" % (width,height))
|
|
|
|
if orien == TextDoc.PAPER_LANDSCAPE:
|
|
file.write("rotate=90;\n")
|
|
|
|
if len(ind_list) > 1:
|
|
dump_index(ind_list,file,includedates,includeurl,colorize,
|
|
arrowheadstyle,arrowtailstyle,show_families)
|
|
dump_person(ind_list,file,adoptionsdashed,arrowheadstyle,
|
|
arrowtailstyle,show_families)
|
|
|
|
file.write("}\n")
|
|
file.close()
|
|
|
|
#------------------------------------------------------------------------
|
|
#
|
|
#
|
|
#
|
|
#------------------------------------------------------------------------
|
|
def dump_person(person_list,file,adoptionsdashed,arrowheadstyle,
|
|
arrowtailstyle,show_families):
|
|
# Hash people in a dictionary for faster inclusion checking.
|
|
person_dict = {}
|
|
for p in person_list:
|
|
person_dict[p.getId()] = 1
|
|
|
|
for person in person_list:
|
|
pid = string.replace(person.getId(),'-','_')
|
|
for family, mrel, frel in person.getParentList():
|
|
father = family.getFather()
|
|
mother = family.getMother()
|
|
fadopted = frel != _("Birth")
|
|
madopted = mrel != _("Birth")
|
|
if (show_families and
|
|
(father and person_dict.has_key(father.getId()) or
|
|
mother and person_dict.has_key(mother.getId()))):
|
|
# Link to the family node.
|
|
famid = string.replace(family.getId(),'-','_')
|
|
file.write('p%s -> f%s [' % (pid, famid))
|
|
file.write('arrowhead=%s, arrowtail=%s, ' %
|
|
(arrowheadstyle, arrowtailstyle))
|
|
if adoptionsdashed and (fadopted or madopted):
|
|
file.write('style=dashed')
|
|
else:
|
|
file.write('style=solid')
|
|
file.write('];\n')
|
|
else:
|
|
# Link to the parents' nodes directly.
|
|
if father and person_dict.has_key(father.getId()):
|
|
fid = string.replace(father.getId(),'-','_')
|
|
file.write('p%s -> p%s [' % (pid, fid))
|
|
file.write('arrowhead=%s, arrowtail=%s, ' %
|
|
(arrowheadstyle, arrowtailstyle))
|
|
if adoptionsdashed and fadopted:
|
|
file.write('style=dashed')
|
|
else:
|
|
file.write('style=solid')
|
|
file.write('];\n')
|
|
if mother and person_dict.has_key(mother.getId()):
|
|
mid = string.replace(mother.getId(),'-','_')
|
|
file.write('p%s -> p%s [' % (pid, mid))
|
|
file.write('arrowhead=%s, arrowtail=%s, ' %
|
|
(arrowheadstyle, arrowtailstyle))
|
|
if adoptionsdashed and madopted:
|
|
file.write('style=dashed')
|
|
else:
|
|
file.write('style=solid')
|
|
file.write('];\n')
|
|
|
|
#------------------------------------------------------------------------
|
|
#
|
|
#
|
|
#
|
|
#------------------------------------------------------------------------
|
|
def dump_index(person_list,file,includedates,includeurl,colorize,
|
|
arrowheadstyle,arrowtailstyle,show_families):
|
|
# The list of families for which we have output the node, so we
|
|
# don't do it twice.
|
|
families_done = []
|
|
for person in person_list:
|
|
# Output the person's node.
|
|
label = person.getPrimaryName().getName()
|
|
id = string.replace(person.getId(),'-','_')
|
|
if includedates:
|
|
if person.getBirth().getDateObj().getYearValid():
|
|
birth = '%i' % person.getBirth().getDateObj().getYear()
|
|
else:
|
|
birth = ''
|
|
if person.getDeath().getDateObj().getYearValid():
|
|
death = '%i' % person.getDeath().getDateObj().getYear()
|
|
else:
|
|
death = ''
|
|
label = label + '\\n(%s - %s)' % (birth, death)
|
|
file.write('p%s [shape=box, ' % id)
|
|
if includeurl:
|
|
file.write('URL="%s.html", ' % id)
|
|
if colorize:
|
|
gender = person.getGender()
|
|
if gender == person.male:
|
|
file.write('color=dodgerblue4, ')
|
|
elif gender == person.female:
|
|
file.write('color=deeppink, ')
|
|
else:
|
|
file.write('color=black, ')
|
|
file.write('fontname="Arial", label="%s"];\n' % utf8_to_latin(label))
|
|
# Output families's nodes.
|
|
if show_families:
|
|
family_list = person.getFamilyList()
|
|
for fam in family_list:
|
|
fid = string.replace(fam.getId(),'-','_')
|
|
if fam not in families_done:
|
|
file.write('f%s [shape=circle, label="", ' % fid)
|
|
file.write('weight=8, height=.3];\n')
|
|
# Link this person to all his/her families.
|
|
file.write('f%s -> p%s [' % (fid, id))
|
|
file.write('arrowhead=%s, arrowtail=%s, ' %
|
|
(arrowheadstyle, arrowtailstyle))
|
|
file.write('style=solid];\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"
|
|
)
|
|
|