gramps/src/plugins/GraphViz.py

1258 lines
50 KiB
Python

#
# Gramps - a GTK+/GNOME based genealogy program
#
# Copyright (C) 2000-2007 Donald N. Allingham
# Cipyright (C) 2007 Johan Gonqvist <johan.gronqvist@gmail.com>
# 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
#
# $Id$
"Generate files/Relationship graph"
#------------------------------------------------------------------------
#
# python modules
#
#------------------------------------------------------------------------
import os
from gettext import gettext as _
from time import asctime
import tempfile
#------------------------------------------------------------------------
#
# Set up logging
#
#------------------------------------------------------------------------
import logging
log = logging.getLogger(".GraphViz")
#------------------------------------------------------------------------
#
# GNOME/gtk
#
#------------------------------------------------------------------------
import gtk
#------------------------------------------------------------------------
#
# GRAMPS modules
#
#------------------------------------------------------------------------
from PluginUtils import register_report
from ReportBase import Report, ReportUtils, ReportOptions, \
CATEGORY_CODE, CATEGORY_DRAW, MODE_GUI, MODE_CLI
from ReportBase._ReportDialog import ReportDialog
from ReportBase._CommandLineReport import CommandLineReport
from Filters import GenericFilter, Rules
import RelLib
import DateHandler
from BasicUtils import name_displayer
import const
from BaseDoc import PAPER_LANDSCAPE
from QuestionDialog import ErrorDialog
import Errors
import Utils
import Mime
#------------------------------------------------------------------------
#
# Constant options items
#
#------------------------------------------------------------------------
class _options:
# internal ID, english option name (for cli), localized option name (for gui)
formats = (
("ps", "Postscript", _("Postscript"), "application/postscript"),
("svg", "Structured Vector Graphics (SVG)", _("Structured Vector Graphics (SVG)"), "image/svg"),
("svgz", "Compressed Structured Vector Graphics (SVG)", _("Compressed Structured Vector Graphs (SVG)"), "image/svgz"),
("png", "PNG image", _("PNG image"), "image/png"),
("jpg", "JPEG image", _("JPEG image"), "image/jpeg"),
("gif", "GIF image", _("GIF image"), "image/gif"),
)
fonts = (
# Last items tells whether strings need to be converted to Latin1
("", "Default", _("Default")),
("Helvetica", "Postscript / Helvetica", _("Postscript / Helvetica")),
("FreeSans", "Truetype / FreeSans", _("Truetype / FreeSans")),
)
colors = (
("outline", "B&W Outline", _("B&W outline")),
("colored", "Colored outline", _("Colored outline")),
("filled", "Color fill", _("Color fill")),
)
ratio = (
("compress", "Minimal size", _("Minimal size")),
("fill", "Fill the given area", _("Fill the given area")),
("expand", "Automatically use optimal number of pages",
_("Automatically use optimal number of pages"))
)
rankdir = (
("TB", "Vertical", _("Vertical")),
("LR", "Horizontal", _("Horizontal")),
)
pagedir = (
("BL", "Bottom, left", _("Bottom, left")),
("BR", "Bottom, right", _("Bottom, right")),
("TL", "Top, left", _("Top, left")),
("TR", "Top, right", _("Top, Right")),
("RB", "Right, bottom", _("Right, bottom")),
("RT", "Right, top", _("Right, top")),
("LB", "Left, bottom", _("Left, bottom")),
("LT", "Left, top", _("Left, top")),
)
noteloc = (
("t", "Top", _("Top")),
("b", "Bottom", _("Bottom")),
)
arrowstyles = (
('d', "Descendants <- Ancestors", _("Descendants <- Ancestors")),
('a', "Descendants -> Ancestors", _("Descendants -> Ancestors")),
('da',"Descendants <-> Ancestors", _("Descendants <-> Ancestors")),
('', "Descendants - Ancestors", _("Descendants - Ancestors")),
)
gs_cmd = ""
if os.sys.platform == "win32":
_dot_found = Utils.search_for("dot.exe")
if Utils.search_for("gswin32c.exe") == 1:
gs_cmd = "gswin32c.exe"
elif Utils.search_for("gswin32.exe") == 1:
gs_cmd = "gswin32.exe"
else:
_dot_found = Utils.search_for("dot")
if Utils.search_for("gs") == 1:
gs_cmd = "gs"
if gs_cmd != "":
_options.formats += (("pdf", "PDF", _("PDF"), "application/pdf"),)
#------------------------------------------------------------------------
#
# Report class
#
#------------------------------------------------------------------------
class GraphViz:
def __init__(self,database,person,options_class):
"""
Creates ComprehensiveAncestorsReport object that produces the report.
The arguments are:
database - the GRAMPS database instance
person - currently selected person
options_class - instance of the Options class for this report
This report needs the following parameters (class variables)
that come in the options class.
filter - Filter to be applied to the people of the database.
The option class carries its number, and the function
returning the list of filters.
font - Font to use.
fontsize - Size of the font in points
latin - Set if text needs to be converted to latin-1
arrow - Arrow styles for heads and tails.
showfamily - Whether to show family nodes.
incid - Whether to include IDs.
incdate - Whether to include dates.
justyears - Use years only.
placecause - Whether to replace missing dates with place or cause
url - Whether to include URLs.
rankdir - Graph direction, LR or RL
ratio - Output aspect ration, fill/compress/auto
color - Whether to use outline, colored outline or filled color in graph
dashedl - Whether to use dashed lines for non-birth relationships.
margin - Margins, in cm.
pagesh - Number of pages in horizontal direction.
pagesv - Number of pages in vertical direction.
pagedir - Paging direction
note - Note to add to the graph
notesize - Note font size (in points)
noteloc - Note location t/b
"""
colored = {
'male': 'dodgerblue4',
'female': 'deeppink',
'unknown': 'black',
'family': 'darkgreen'
}
filled = {
'male': 'lightblue',
'female': 'lightpink',
'unknown': 'lightgray',
'family': 'lightyellow'
}
self.database = database
self.start_person = person
self.paper = options_class.handler.get_paper()
self.orient = options_class.handler.get_orientation()
self.width = self.paper.get_width_inches()
self.height = self.paper.get_height_inches()
options = options_class.handler.options_dict
self.pagedir = options['pagedir']
self.hpages = options['pagesh']
self.vpages = options['pagesv']
margin_cm = options['margin']
self.margin = round(margin_cm/2.54,2)
if margin_cm > 0.1:
# GraphViz has rounding errors so have to make the real
# margins slightly smaller than (page - content size)
self.margin_small = round((margin_cm-0.1)/2.54,2)
else:
self.margin_small = 0
self.includeid = options['incid']
self.includedates = options['incdate']
self.includeurl = options['url']
self.adoptionsdashed = options['dashedl']
self.show_families = options['showfamily']
self.just_years = options['justyears']
self.placecause = options['placecause']
self.rankdir = options['rankdir']
self.ratio = options['ratio']
self.fontname = options['font']
self.fontsize = options['fontsize']
self.colorize = options['color']
if self.colorize == 'colored':
self.colors = colored
elif self.colorize == 'filled':
self.colors = filled
arrow_str = options['arrow']
if arrow_str.find('a') + 1:
self.arrowheadstyle = 'normal'
else:
self.arrowheadstyle = 'none'
if arrow_str.find('d') + 1:
self.arrowtailstyle = 'normal'
else:
self.arrowtailstyle = 'none'
self.latin = options['latin']
self.noteloc = options['noteloc']
self.notesize = options['notesize']
self.note = options['note']
filter_num = options_class.handler.options_dict['filter']
filters = ReportUtils.get_person_filters(person,include_single=False)
self.filter = filters[filter_num]
the_buffer = self.get_report()
self.f = open(options_class.get_output(),'w')
if self.latin:
try:
self.f.write(the_buffer.encode('iso-8859-1', 'strict'))
except UnicodeEncodeError:
self.f = open(options_class.get_output(),'w')
self.f.write(the_buffer.encode('iso-8859-1', 'replace'))
ErrorDialog(
_("Your data contains characters that cannot be "
"converted to latin-1. These characters were "
"replaced with the question marks in the output. "
"To get these characters properly displayed, "
"unselect latin-1 option and try again."))
else:
self.f.write(the_buffer)
self.f.close()
def get_report(self):
"return string of the .dot file contents"
self.person_handles = self.filter.apply(self.database,
self.database.get_person_handles(sort_handles=False))
# graph size
if self.orient == PAPER_LANDSCAPE:
rotate = 90
sizew = (self.height - self.margin*2) * self.hpages
sizeh = (self.width - self.margin*2) * self.vpages
else:
rotate = 0
sizew = (self.width - self.margin*2) * self.hpages
sizeh = (self.height - self.margin*2) * self.vpages
buffer = self.get_comment_header()
buffer += """
digraph GRAMPS_relationship_graph {
/* whole graph attributes */
bgcolor=white;
center=1;
ratio=%s;
rankdir=%s;
mclimit=2.0;
margin="%3.2f";
pagedir="%s";
page="%3.2f,%3.2f";
size="%3.2f,%3.2f";
rotate=%d;
/* default node and edge attributes */
nodesep=0.25;
edge [style=solid, arrowhead=%s arrowtail=%s];
""" % (
self.ratio,
self.rankdir,
self.margin_small,
self.pagedir,
self.width, self.height,
sizew, sizeh,
rotate,
self.arrowheadstyle,
self.arrowtailstyle
)
if self.fontname:
font = 'fontname="%s" ' % self.fontname
else:
font = ''
font += 'fontsize="%d"' % self.fontsize
if self.colorize == 'filled':
buffer += 'node [style=filled %s];\n' % font
else:
buffer += 'node [%s];\n' % font
if self.latin:
# GraphViz default is UTF-8
buffer += 'charset="iso-8859-1";\n'
if len(self.person_handles) > 1:
buffer += "/* persons and their families */\n"
buffer += self.get_persons_and_families()
buffer += "/* link children to families */\n"
buffer += self.get_child_links_to_families()
if self.note:
buffer += 'labelloc="%s";\n' % self.noteloc
buffer += 'label="%s";\n' % self.note.replace('\n', '\\n').replace('"', '\\\"')
buffer += 'fontsize="%d";\n' % self.notesize # in points
return buffer + "}\n"
def get_comment_header(self):
"return comment of Gramps options which are not Graphviz options"
return """/*
GRAMPS - Relationship graph
Generated on %s.
Report content options:
include URLs : %s
IDs : %s
dates : %s
just year : %s
place or cause : %s
colorize : %s
dotted adoptions : %s
show family nodes : %s
pages horizontal : %s
vertical : %s
For other options, see graph settings below.
If you need to switch between iso-8859-1 and utf-8 text encodings,
e.g. because you're using different font or -T output format,
just use iconv:
iconv -f iso-8859-1 -t utf-8 old.dot > new.dot
iconv -t utf-8 -f iso-8859-1 old.dot > new.dot
*/
""" % (
asctime(),
bool(self.includeurl),
bool(self.includeid),
bool(self.includedates),
bool(self.just_years),
bool(self.placecause),
bool(self.colorize),
bool(self.adoptionsdashed),
bool(self.show_families),
self.hpages, self.vpages
)
def get_child_links_to_families(self):
"returns string of GraphViz edges linking parents to families or children"
person_dict = {}
# Hash people in a dictionary for faster inclusion checking
for person_handle in self.person_handles:
person_dict[person_handle] = 1
the_buffer = ""
for person_handle in self.person_handles:
person = self.database.get_person_from_handle(person_handle)
p_id = person.get_gramps_id()
for fam_handle in person.get_parent_family_handle_list():
family = self.database.get_family_from_handle(fam_handle)
father_handle = family.get_father_handle()
mother_handle = family.get_mother_handle()
for child_ref in family.get_child_ref_list():
if child_ref.ref == person_handle:
frel = child_ref.frel
mrel = child_ref.mrel
break
if (self.show_families and
((father_handle and father_handle in person_dict) or
(mother_handle and mother_handle in person_dict))):
# Link to the family node if either parent is in graph
the_buffer += self.get_family_link(p_id,family,frel,mrel)
else:
# Link to the parents' nodes directly, if they are in graph
if father_handle and father_handle in person_dict:
the_buffer += self.get_parent_link(p_id,father_handle,frel)
if mother_handle and mother_handle in person_dict:
the_buffer += self.get_parent_link(p_id,mother_handle,mrel)
return the_buffer
def get_family_link(self, p_id, family, frel, mrel):
"returns string of GraphViz edge linking child to family"
style = ''
adopted = ((int(frel) != RelLib.ChildRefType.BIRTH) or
(int(mrel) != RelLib.ChildRefType.BIRTH))
if adopted and self.adoptionsdashed:
style = 'style=dotted'
return '"p%s" -> "f%s" [%s];\n' % (p_id,
family.get_gramps_id(), style)
def get_parent_link(self, p_id, parent_handle, rel):
"returns string of GraphViz edge linking child to parent"
style = ''
if (int(rel) != RelLib.ChildRefType.BIRTH) and self.adoptionsdashed:
style = 'style=dotted'
parent = self.database.get_person_from_handle(parent_handle)
return '"p%s" -> "p%s" [%s];\n' % (p_id, parent.get_gramps_id(), style)
def get_persons_and_families(self):
"returns string of GraphViz nodes for persons and their families"
# The list of families for which we have output the node,
# so we don't do it twice
buffer = ""
families_done = {}
for person_handle in self.person_handles:
person = self.database.get_person_from_handle(person_handle)
p_id = person.get_gramps_id()
# Output the person's node
label = self.get_person_label(person)
style = self.get_gender_style(person)
url = ""
if self.includeurl:
h = person_handle
dirpath = "ppl/%s/%s" % (h[0], h[1])
if os.sys.platform == "win32":
dirpath = dirpath.lower()
url = ', URL="%s/%s.html", ' % (dirpath,h)
buffer += '"p%s" [label="%s", %s%s];\n' % (p_id, label, style, url)
# Output families where person is a parent
if self.show_families:
family_list = person.get_family_handle_list()
for fam_handle in family_list:
fam = self.database.get_family_from_handle(fam_handle)
fam_id = fam.get_gramps_id()
if fam_handle not in families_done:
families_done[fam_handle] = 1
label = ""
for event_ref in fam.get_event_ref_list():
event = self.database.get_event_from_handle(
event_ref.ref)
if int(event.get_type()) == RelLib.EventType.MARRIAGE:
label = self.get_event_string(event)
break
if self.includeid:
label = "%s (%s)" % (label, fam_id)
color = ""
if self.colorize == 'colored':
color = ', color="%s"' % self.colors['family']
elif self.colorize == 'filled':
color = ', fillcolor="%s"' % self.colors['family']
buffer += '"f%s" [shape=ellipse, label="%s"%s];\n' % (fam_id, label, color)
# Link this person to all his/her families.
buffer += '"f%s" -> "p%s";\n' % (fam_id, p_id)
return buffer
def get_gender_style(self, person):
"return gender specific person style"
gender = person.get_gender()
if gender == person.MALE:
shape = 'shape="box"'
elif gender == person.FEMALE:
shape = 'shape="box", style="rounded"'
else:
shape = 'shape="hexagon"'
if self.colorize == 'outline':
return shape
else:
if gender == person.MALE:
color = self.colors['male']
elif gender == person.FEMALE:
color = self.colors['female']
else:
color = self.colors['unknown']
if self.colorize == 'filled':
# In current GraphViz boxes cannot be both rounded and filled
return 'shape="box", fillcolor="%s"' % color
else:
return '%s, color="%s"' % (shape, color)
def get_person_label(self, person):
"return person label string"
label = name_displayer.display_name(person.get_primary_name())
p_id = person.get_gramps_id()
if self.includeid:
label = label + " (%s)" % p_id
if self.includedates:
birth, death = self.get_date_strings(person)
label = label + '\\n(%s - %s)' % (birth, death)
return label.replace('"', '\\\"')
def get_date_strings(self, person):
"returns tuple of birth/christening and death/burying date strings"
birth_ref = person.get_birth_ref()
if birth_ref:
birth_event = self.database.get_event_from_handle(birth_ref.ref)
birth = self.get_event_string(birth_event)
else:
birth = ''
death_ref = person.get_death_ref()
if death_ref:
death_event = self.database.get_event_from_handle(death_ref.ref)
death = self.get_event_string(death_event)
else:
death = ''
if birth and death:
return (birth, death)
# missing info, use (first) christening/burial instead
for event_ref in person.get_primary_event_ref_list():
event = self.database.get_event_from_handle(event_ref.ref)
if int(event.get_type()) == RelLib.EventType.CHRISTEN:
if not birth:
birth = self.get_event_string(event)
elif int(event.get_type()) == RelLib.EventType.BURIAL:
if not death:
death = self.get_event_string(event)
return (birth, death)
def get_event_string(self, event):
"""
return string for for an event label.
Based on the data availability and preferences, we select one
of the following for a given event:
year only
complete date
place name
cause
empty string
"""
if event:
if event.get_date_object().get_year_valid():
if self.just_years:
return '%i' % event.get_date_object().get_year()
else:
return DateHandler.get_date(event)
elif self.placecause:
place_handle = event.get_place_handle()
place = self.database.get_place_from_handle(place_handle)
if place and place.get_title():
return place.get_title()
else:
return '' #event.get_cause()
return ''
#------------------------------------------------------------------------
#
# Options class
#
#------------------------------------------------------------------------
class GraphVizOptions(ReportOptions):
"""
Defines options and provides handling interface.
"""
def __init__(self,name,person_id=None):
ReportOptions.__init__(self,name,person_id)
def set_new_options(self):
# Options specific for this report
self.options_dict = {
'filter' : 0,
'font' : "",
'fontsize' : 14,
'latin' : 1,
'arrow' : 'd',
'showfamily' : 1,
'incdate' : 1,
'incid' : 0,
'justyears' : 0,
'placecause' : 1,
'url' : 1,
'ratio' : "compress",
'rankdir' : "LR",
'color' : "filled",
'dashedl' : 1,
'margin' : 1.0,
'pagedir' : 'BL',
'pagesh' : 1,
'pagesv' : 1,
'note' : '',
'noteloc' : 'b',
'notesize' : 32,
'gvof' : 'ps',
}
filters = ReportUtils.get_person_filters(None,include_single=False)
self.options_help = {
'filter' : ("=num","Filter number.",
[ filt.get_name() for filt in filters ],
True ),
'font' : ("=str","Font to use in the report.",
[ "%s\t%s" % (item[0],item[1]) for item in _options.fonts ],
False),
'fontsize' : ("=num","Font size (in points).",
"Integer values"),
'latin' : ("=0/1","Needs to be set if font doesn't support unicode.",
["Supports unicode","Supports only Latin1"],
True),
'arrow' : ("=str","Arrow styles for heads and tails.",
[ "%s\t%s" % (item[0],item[1]) for item in _options.arrowstyles ],
False),
'showfamily': ("=0/1","Whether to show family nodes.",
["Do not show family nodes","Show family nodes"],
True),
'incdate' : ("=0/1","Whether to include dates.",
["Do not include dates","Include dates"],
True),
'incid' : ("=0/1","Whether to include IDs.",
["Do not include IDs","Include IDs"],
True),
'justyears' : ("=0/1","Whether to use years only.",
["Do not use years only","Use years only"],
True),
'placecause': ("=0/1","Whether to replace missing dates with place/cause.",
["Do not replace blank dates","Replace blank dates"],
True),
'url' : ("=0/1","Whether to include URLs.",
["Do not include URLs","Include URLs"],
True),
'ratio' : ("=str","Graph aspect ratio.",
[ "%s\t%s" % (item[0],item[1]) for item in _options.ratio ],
False),
'rankdir' : ("=str","Graph direction.",
[ "%s\t%s" % (item[0],item[1]) for item in _options.rankdir ],
False),
'color' : ("=str","Whether and how to colorize graph.",
[ "%s\t%s" % (item[0],item[1]) for item in _options.colors ],
False),
'dashedl' : ("=0/1","Whether to use dotted lines for non-birth relationships.",
["Do not use dotted lines","Use dotted lines"],
True),
'margin' : ("=num","Margin size.",
"Floating point value, in cm"),
'pagedir' : ("=str","Paging direction.",
[ "%s\t%s" % (item[0],item[1]) for item in _options.pagedir ],
False),
'pagesh' : ("=num","Number of pages in horizontal direction.",
"Integer values"),
'pagesv' : ("=num","Number of pages in vertical direction.",
"Integer values"),
'note' : ("=str","Note to add to the graph.",
"Text"),
'notesize' : ("=num","Note size (in points).",
"Integer values"),
'noteloc' : ("=str","Note location.",
[ "%s\t%s" % (item[0],item[1]) for item in _options.noteloc ],
False),
'gvof' : ("=str","Output format to convert dot file into.",
[ "%s\t%s" % (item[0],item[1]) for item in _options.formats ],
False),
}
def make_doc_menu(self,dialog,active=None):
pass
def add_list(self, options, default):
"returns compobox of given options and default value"
box = gtk.ComboBox()
store = gtk.ListStore(str)
box.set_model(store)
cell = gtk.CellRendererText()
box.pack_start(cell,True)
box.add_attribute(cell,'text',0)
index = 0
for item in options:
store.append(row=[item[2]])
if item[0] == default:
box.set_active(index)
index = index + 1
return box
def add_user_options(self,dialog):
if self.handler.module_name == "rel_graph2":
dialog.make_doc_menu = self.make_doc_menu
dialog.format_menu = GraphicsFormatComboBox()
dialog.format_menu.set(self.options_dict['gvof'])
filter_index = self.options_dict['filter']
filter_list = ReportUtils.get_person_filters(dialog.person,include_single=False)
self.filter_menu = gtk.combo_box_new_text()
for filter in filter_list:
self.filter_menu.append_text(filter.get_name())
if filter_index > len(filter_list):
filter_index = 0
self.filter_menu.set_active(filter_index)
dialog.add_option('Filter',self.filter_menu)
# Content options tab
msg = _("Include Birth, Marriage and Death dates")
self.includedates_cb = gtk.CheckButton(msg)
self.includedates_cb.set_active(self.options_dict['incdate'])
dialog.add_option(None,
self.includedates_cb,
_("Include the dates that the individual "
"was born, got married and/or died "
"in the graph labels."))
self.just_years_cb = gtk.CheckButton(_("Limit dates to years only"))
self.just_years_cb.set_active(self.options_dict['justyears'])
dialog.add_option(None,
self.just_years_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.options_dict['placecause'])
dialog.add_option(None,
self.place_cause_cb,
_("When no birth, marriage, or death date "
"is available, the correspondent place field "
"(or cause field when blank place) will be used."))
# disable other date options if no dates
self.includedates_cb.connect('toggled',self.toggle_date)
self.toggle_date(self.includedates_cb)
self.includeurl_cb = gtk.CheckButton(_("Include URLs"))
self.includeurl_cb.set_active(self.options_dict['url'])
dialog.add_option(None,
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.includeid_cb = gtk.CheckButton(_("Include IDs"))
self.includeid_cb.set_active(self.options_dict['incid'])
dialog.add_option(None,
self.includeid_cb,
_("Include individual and family IDs."))
# GraphViz output options tab
self.color_box = self.add_list(_options.colors,
self.options_dict['color'])
dialog.add_frame_option(_("GraphViz Options"),
_("Graph coloring"),
self.color_box,
_("Males will be shown with blue, females "
"with red. If the sex of an individual "
"is unknown it will be shown with gray."))
self.arrowstyle_box = self.add_list(_options.arrowstyles,
self.options_dict['arrow'])
dialog.add_frame_option(_("GraphViz Options"),
_("Arrowhead direction"),
self.arrowstyle_box,
_("Choose the direction that the arrows point."))
self.font_box = self.add_list(_options.fonts,
self.options_dict['font'])
dialog.add_frame_option(_("GraphViz Options"),
_("Font family"),
self.font_box,
_("Choose the font family. If international "
"characters don't show, use FreeSans font. "
"FreeSans is available from: "
"http://www.nongnu.org/freefont/"))
fontsize_adj = gtk.Adjustment(value=self.options_dict['fontsize'],
lower=8, upper=128, step_incr=1)
self.fontsize_sb = gtk.SpinButton(adjustment=fontsize_adj, digits=0)
dialog.add_frame_option(_("GraphViz Options"),
_("Font size (in points)"),
self.fontsize_sb,
_("The font size, in points."))
self.latin_cb = gtk.CheckButton(_("Output format/font requires text as latin-1"))
self.latin_cb.set_active(self.options_dict['latin'])
dialog.add_frame_option(_("GraphViz Options"), '',
self.latin_cb,
_("If text doesn't show correctly in report, use this. "
"Required e.g. for default font with PS output. "
"Not typically required for SVG or JPG output."))
self.adoptionsdashed_cb = gtk.CheckButton(_("Indicate non-birth relationships with dotted lines"))
self.adoptionsdashed_cb.set_active(self.options_dict['dashedl'])
dialog.add_frame_option(_("GraphViz Options"), '',
self.adoptionsdashed_cb,
_("Non-birth relationships will show up "
"as dotted lines in the graph."))
self.show_families_cb = gtk.CheckButton(_("Show family nodes"))
self.show_families_cb.set_active(self.options_dict['showfamily'])
dialog.add_frame_option(_("GraphViz Options"), '',
self.show_families_cb,
_("Families will show up as ellipses, linked "
"to parents and children."))
# Page/layout options tab
self.rank_box = self.add_list(_options.rankdir,
self.options_dict['rankdir'])
dialog.add_frame_option(_("Layout Options"),
_("Graph direction"),
self.rank_box,
_("Whether generations go from top to bottom "
"or left to right."))
self.ratio_box = self.add_list(_options.ratio,
self.options_dict['ratio'])
dialog.add_frame_option(_("Layout Options"),
_("Aspect ratio"),
self.ratio_box,
_("Affects greatly how the graph is layed out "
"on the page. Multiple pages overrides the "
"pages settings below."))
margin_adj = gtk.Adjustment(value=self.options_dict['margin'],
lower=0.0, upper=10.0, step_incr=1.0)
self.margin_sb = gtk.SpinButton(adjustment=margin_adj, digits=1)
dialog.add_frame_option(_("Layout Options"),
_("Margin size"),
self.margin_sb)
hpages_adj = gtk.Adjustment(value=self.options_dict['pagesh'],
lower=1, upper=25, step_incr=1)
vpages_adj = gtk.Adjustment(value=self.options_dict['pagesv'],
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)
dialog.add_frame_option(_("Layout 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."))
dialog.add_frame_option(_("Layout 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."))
self.pagedir_box = self.add_list(_options.pagedir,
self.options_dict['pagedir'])
dialog.add_frame_option(_("Layout Options"),
_("Paging direction"),
self.pagedir_box,
_("The order in which the graph pages are output."))
# Notes tab
self.textbox = gtk.TextView()
self.textbox.get_buffer().set_text(self.options_dict['note'])
self.textbox.set_editable(1)
swin = gtk.ScrolledWindow()
swin.set_shadow_type(gtk.SHADOW_IN)
swin.set_policy(gtk.POLICY_AUTOMATIC,gtk.POLICY_AUTOMATIC)
swin.add(self.textbox)
dialog.add_frame_option(_("Notes"),
_("Note to add to the graph"),
swin,
_("This text will be added to the graph."))
self.noteloc_box = self.add_list(_options.noteloc,
self.options_dict['noteloc'])
dialog.add_frame_option(_("Notes"),
_("Note location"),
self.noteloc_box,
_("Whether note will appear on top "
"or bottom of the page."))
notesize_adj = gtk.Adjustment(value=self.options_dict['notesize'],
lower=8, upper=128, step_incr=1)
self.notesize_sb = gtk.SpinButton(adjustment=notesize_adj, digits=0)
dialog.add_frame_option(_("Notes"),
_("Note size (in points)"),
self.notesize_sb,
_("The size of note text, in points."))
def toggle_date(self, obj):
self.just_years_cb.set_sensitive(self.includedates_cb.get_active())
self.place_cause_cb.set_sensitive(self.includedates_cb.get_active())
def parse_user_options(self,dialog):
self.options_dict['filter'] = int(self.filter_menu.get_active())
self.options_dict['incdate'] = int(self.includedates_cb.get_active())
self.options_dict['url'] = int(self.includeurl_cb.get_active())
self.options_dict['margin'] = self.margin_sb.get_value()
self.options_dict['dashedl'] = int(self.adoptionsdashed_cb.get_active())
self.options_dict['pagesh'] = self.hpages_sb.get_value_as_int()
self.options_dict['pagesv'] = self.vpages_sb.get_value_as_int()
self.options_dict['showfamily'] = int(self.show_families_cb.get_active())
self.options_dict['fontsize'] = self.fontsize_sb.get_value_as_int()
self.options_dict['incid'] = int(self.includeid_cb.get_active())
self.options_dict['justyears'] = int(self.just_years_cb.get_active())
self.options_dict['placecause'] = int(self.place_cause_cb.get_active())
self.options_dict['latin'] = int(self.latin_cb.get_active())
self.options_dict['ratio'] = \
_options.ratio[self.ratio_box.get_active()][0]
self.options_dict['rankdir'] = \
_options.rankdir[self.rank_box.get_active()][0]
self.options_dict['color'] = \
_options.colors[self.color_box.get_active()][0]
self.options_dict['arrow'] = \
_options.arrowstyles[self.arrowstyle_box.get_active()][0]
self.options_dict['font'] = \
_options.fonts[self.font_box.get_active()][0]
self.options_dict['pagedir'] = \
_options.pagedir[self.pagedir_box.get_active()][0]
self.options_dict['noteloc'] = \
_options.noteloc[self.noteloc_box.get_active()][0]
self.options_dict['notesize'] = self.notesize_sb.get_value_as_int()
b = self.textbox.get_buffer()
self.options_dict['note'] = \
b.get_text(b.get_start_iter(), b.get_end_iter(), False)
if self.handler.module_name == "rel_graph2":
self.options_dict['gvof'] = dialog.format_menu.get_format_str()
#------------------------------------------------------------------------
#
# Dialog class
#
#------------------------------------------------------------------------
class GraphVizDialog(ReportDialog):
def __init__(self,dbstate,uistate,person):
self.database = dbstate.db
self.person = person
name = "rel_graph"
translated_name = _("Relationship Graph")
self.options_class = GraphVizOptions(name)
self.category = CATEGORY_CODE
ReportDialog.__init__(self,dbstate,uistate,person,self.options_class,
name,translated_name)
response = self.window.run()
if response == gtk.RESPONSE_OK:
try:
self.make_report()
except (IOError,OSError),msg:
ErrorDialog(str(msg))
elif response == gtk.RESPONSE_DELETE_EVENT:
return
self.close()
def make_doc_menu(self,active=None):
"""Build a one item menu of document types that are
appropriate for this report."""
self.format_menu = FormatComboBox()
self.format_menu.set()
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 make_report(self):
"""Create the object that will produce the GraphViz file."""
GraphViz(self.database,self.person,self.options_class)
#------------------------------------------------------------------------
#
# Combo Box classes
#
#------------------------------------------------------------------------
class FormatComboBox(gtk.ComboBox):
"""
Format combo box class.
Trivial class supporting only one format.
"""
def set(self,tables=0,callback=None,obj=None,active=None):
self.store = gtk.ListStore(str)
self.set_model(self.store)
cell = gtk.CellRendererText()
self.pack_start(cell,True)
self.add_attribute(cell,'text',0)
self.store.append(row=["Graphviz (dot)"])
self.set_active(0)
def get_label(self):
return "Graphviz (dot)"
def get_reference(self):
return None
def get_paper(self):
return 1
def get_styles(self):
return 0
def get_ext(self):
return '.dot'
def get_printable(self):
_apptype = _options.formats[self.get_active()][3]
print_label = None
try:
mprog = Mime.get_application(_apptype)
if Utils.search_for(mprog[0]):
print_label = _("Open in %(program_name)s") % { 'program_name':
mprog[1]}
else:
print_label = None
except:
print_label = None
return print_label
def get_clname(self):
return 'dot'
class GraphicsFormatComboBox(gtk.ComboBox):
"""
Format combo box class for graphical (not codegen) report.
"""
def set(self,active=None):
self.store = gtk.ListStore(str)
self.set_model(self.store)
cell = gtk.CellRendererText()
self.pack_start(cell,True)
self.add_attribute(cell,'text',0)
active_index = 0
index = 0
for item in _options.formats:
self.store.append(row=[item[2]])
if active == item[0]:
active_index = index
index = index + 1
self.set_active(active_index)
def get_label(self):
return _options.formats[self.get_active()][2]
def get_reference(self):
return EmptyDoc
def get_paper(self):
return 1
def get_styles(self):
return 0
def get_ext(self):
return '.%s' % _options.formats[self.get_active()][0]
def get_format_str(self):
return _options.formats[self.get_active()][0]
def get_printable(self):
_apptype = _options.formats[self.get_active()][3]
print_label = None
try:
mprog = Mime.get_application(_apptype)
if Utils.search_for(mprog[0]):
print_label = _("Open in %(program_name)s") % { 'program_name':
mprog[1] }
else:
print_label = None
except:
print_label = None
return print_label
def get_clname(self):
return 'print'
#------------------------------------------------------------------------
#
# Empty class to keep the BaseDoc-targeted format happy
#
#------------------------------------------------------------------------
class EmptyDoc:
def __init__(self,styles,type,template,source=None):
self.print_req = 0
def init(self):
pass
def print_requested(self):
self.print_req = 1
#------------------------------------------------------------------------
#
#
#
#------------------------------------------------------------------------
def cl_report(database,name,category,options_str_dict):
clr = CommandLineReport(database,name,category,GraphVizOptions,
options_str_dict)
# Exit here if show option was given
if clr.show:
return
GraphViz(database,clr.person,clr.option_class)
#------------------------------------------------------------------------
#
#
#
#------------------------------------------------------------------------
class GraphVizGraphics(Report):
def __init__(self,database,person,options_class):
self.database = database
self.start_person = person
self.options_class = options_class
self.doc = options_class.get_document()
self.user_output = options_class.get_output()
(handle,self.junk_output) = tempfile.mkstemp(".dot", "rel_graph" )
os.close( handle )
self.the_format = self.options_class.handler.options_dict['gvof']
self.the_font = self.options_class.handler.options_dict['font']
def begin_report(self):
self.options_class.set_output(self.junk_output)
def write_report(self):
GraphViz(self.database,self.start_person,self.options_class)
def end_report(self):
if self.the_format == "pdf":
# Create a temporary Postscript file
(handle,tmp_ps) = tempfile.mkstemp(".ps", "rel_graph" )
os.close( handle )
# Generate Postscript using dot
command = 'dot -Tps -o"%s" "%s"' % ( tmp_ps, self.junk_output )
os.system(command)
paper = self.options_class.handler.get_paper()
# Add .5 to remove rounding errors.
width_pt = int( (paper.get_width_inches() * 72) + 0.5 )
height_pt = int( (paper.get_height_inches() * 72) + 0.5 )
# Convert to PDF using ghostscript
command = '%s -q -sDEVICE=pdfwrite -dNOPAUSE -dDEVICEWIDTHPOINTS=%d -dDEVICEHEIGHTPOINTS=%d -sOutputFile="%s" "%s" -c quit' \
% ( gs_cmd, width_pt, height_pt, self.user_output, tmp_ps )
os.system(command)
os.remove(tmp_ps)
else:
os.system('dot -T%s -o"%s" "%s"' %
(self.the_format,self.user_output,self.junk_output) )
os.remove(self.junk_output)
if self.doc.print_req:
_apptype = None
for format in _options.formats:
if format[0] == self.the_format:
_apptype = format[3]
break
if _apptype:
try:
app = Mime.get_application(_apptype)
Utils.launch(app[0],self.user_output)
except:
pass
#------------------------------------------------------------------------
#
#
#
#------------------------------------------------------------------------
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")
def get_description_graphics():
return _("Generates relationship graphs using GraphViz (dot) program. "
"This report generates dot file behind the scene and then "
"uses dot to convert it into a graph. If you want the dot"
"file itself, please use the Code Generators category.")
#------------------------------------------------------------------------
#
#
#
#------------------------------------------------------------------------
register_report(
name = 'rel_graph',
category = CATEGORY_CODE,
report_class = GraphVizDialog,
options_class = cl_report,
modes = MODE_GUI | MODE_CLI,
translated_name = _("Relationship Graph"),
status = _("Stable"),
description= get_description(),
author_name="Donald N. Allingham",
author_email="don@gramps-project.org"
)
if _dot_found:
register_report(
name = 'rel_graph2',
category = CATEGORY_DRAW,
report_class = GraphVizGraphics,
options_class = GraphVizOptions,
modes = MODE_GUI | MODE_CLI,
translated_name = _("Relationship Graph"),
status = _("Stable"),
description= get_description_graphics(),
author_name="Donald N. Allingham",
author_email="don@gramps-project.org"
)