2
# Gramps - a GTK+/GNOME based genealogy program
4
# Copyright (C) 2000-2004 Donald N. Allingham
6
# This program is free software; you can redistribute it and/or modify
7
# it under the terms of the GNU General Public License as published by
8
# the Free Software Foundation; either version 2 of the License, or
9
# (at your option) any later version.
11
# This program is distributed in the hope that it will be useful,
12
# but WITHOUT ANY WARRANTY; without even the implied warranty of
13
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14
# GNU General Public License for more details.
16
# You should have received a copy of the GNU General Public License
17
# along with this program; if not, write to the Free Software
18
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
21
# $Id: GraphViz.py,v 1.23.2.3 2004/03/19 00:29:04 rshura Exp $
23
"Generate files/Relationship graph"
25
#------------------------------------------------------------------------
29
#------------------------------------------------------------------------
33
#------------------------------------------------------------------------
37
#------------------------------------------------------------------------
40
#------------------------------------------------------------------------
44
#------------------------------------------------------------------------
51
from gettext import gettext as _
52
from latin_utf8 import utf8_to_latin
54
#------------------------------------------------------------------------
58
#------------------------------------------------------------------------
60
_PS_FONT = 'Helvetica'
63
#------------------------------------------------------------------------
67
#------------------------------------------------------------------------
68
class GraphVizDialog(Report.ReportDialog):
72
def __init__(self,database,person):
74
Report.ReportDialog.__init__(self,database,person,self.report_options)
77
"""The window title for this dialog"""
78
return "%s - %s - GRAMPS" % (_("Relationship Graph"),
79
_("Graphical Reports"))
81
def get_target_browser_title(self):
82
"""The title of the window created when the 'browse' button is
83
clicked in the 'Save As' frame."""
84
return _("Graphviz File")
86
def get_report_generations(self):
87
"""Default to 10 generations, no page breaks."""
90
def get_report_filters(self):
91
"""Set up the list of possible content filters."""
93
name = self.person.getPrimaryName().getName()
95
all = GenericFilter.GenericFilter()
96
all.set_name(_("Entire Database"))
97
all.add_rule(GenericFilter.Everyone([]))
99
des = GenericFilter.GenericFilter()
100
des.set_name(_("Descendants of %s") % name)
101
des.add_rule(GenericFilter.IsDescendantOf([self.person.getId(),1]))
103
ans = GenericFilter.GenericFilter()
104
ans.set_name(_("Ancestors of %s") % name)
105
ans.add_rule(GenericFilter.IsAncestorOf([self.person.getId(),1]))
107
com = GenericFilter.GenericFilter()
108
com.set_name(_("People with common ancestor with %s") % name)
109
com.add_rule(GenericFilter.HasCommonAncestorWith([self.person.getId()]))
111
return [all,des,ans,com]
113
def add_user_options(self):
114
self.arrowstyle_optionmenu = gtk.OptionMenu()
117
menuitem = gtk.MenuItem(_("Descendants <- Ancestors"))
118
menuitem.set_data('t', ('none', 'normal'))
120
menu.append(menuitem)
122
menuitem = gtk.MenuItem(_("Descendants -> Ancestors"))
123
menuitem.set_data('t', ('normal', 'none'))
125
menu.append(menuitem)
127
menuitem = gtk.MenuItem(_("Descendants <-> Ancestors"))
128
menuitem.set_data('t', ('normal', 'normal'))
130
menu.append(menuitem)
132
menuitem = gtk.MenuItem(_("Descendants - Ancestors"))
133
menuitem.set_data('t', ('none', 'none'))
135
menu.append(menuitem)
139
self.arrowstyle_optionmenu.set_menu(menu)
141
self.font_optionmenu = gtk.OptionMenu()
144
menuitem = gtk.MenuItem(_("TrueType"))
145
menuitem.set_data('t', _TT_FONT)
147
menu.append(menuitem)
149
menuitem = gtk.MenuItem(_("PostScript"))
150
menuitem.set_data('t', _PS_FONT)
152
menu.append(menuitem)
154
self.font_optionmenu.set_menu(menu)
156
self.add_frame_option(_("GraphViz Options"),
158
self.font_optionmenu,
159
_("Choose the font family."))
161
self.add_frame_option(_("GraphViz Options"),
162
_("Arrowhead Options"),
163
self.arrowstyle_optionmenu,
164
_("Choose the direction that the arrows point."))
166
msg = _("Include Birth, Marriage and Death Dates")
167
self.includedates_cb = gtk.CheckButton(msg)
168
self.includedates_cb.set_active(1)
169
self.add_frame_option(_("GraphViz Options"), '',
170
self.includedates_cb,
171
_("Include the dates that the individual "
172
"was born, got married and/or died "
173
"in the graph labels."))
175
self.just_year_cb = gtk.CheckButton(_("Limit dates to years only"))
176
self.just_year_cb.set_active(0)
177
self.add_frame_option(_("GraphViz Options"), '',
179
_("Prints just dates' year, neither "
180
"month or day nor date approximation "
181
"or interval are shown."))
183
self.includedates_cb.connect('toggled',self.toggle_date)
185
self.includeurl_cb = gtk.CheckButton(_("Include URLs"))
186
self.includeurl_cb.set_active(1)
187
self.add_frame_option(_("GraphViz Options"), '',
189
_("Include a URL in each graph node so "
190
"that PDF and imagemap files can be "
191
"generated that contain active links "
192
"to the files generated by the 'Generate "
193
"Web Site' report."))
195
self.colorize_cb = gtk.CheckButton(_("Colorize Graph"))
196
self.colorize_cb.set_active(1)
197
self.add_frame_option(_("GraphViz Options"),
200
_("Males will be outlined in blue, females "
201
"will be outlined in pink. If the sex of "
202
"an individual is unknown it will be "
203
"outlined in black."))
205
self.adoptionsdashed_cb = gtk.CheckButton(_("Indicate non-birth relationships with dashed lines"))
206
self.adoptionsdashed_cb.set_active(1)
207
self.add_frame_option(_("GraphViz Options"),
209
self.adoptionsdashed_cb,
210
_("Non-birth relationships will show up "
211
"as dashed lines in the graph."))
213
self.show_families_cb = gtk.CheckButton(_("Show family nodes"))
214
self.show_families_cb.set_active(0)
215
self.add_frame_option(_("GraphViz Options"),
217
self.show_families_cb,
218
_("Families will show up as ellipses, linked "
219
"to parents and children."))
221
tb_margin_adj = gtk.Adjustment(value=0.5, lower=0.25,
222
upper=100.0, step_incr=0.25)
223
lr_margin_adj = gtk.Adjustment(value=0.5, lower=0.25,
224
upper=100.0, step_incr=0.25)
226
self.tb_margin_sb = gtk.SpinButton(adjustment=tb_margin_adj, digits=2)
227
self.lr_margin_sb = gtk.SpinButton(adjustment=lr_margin_adj, digits=2)
229
self.add_frame_option(_("Page Options"),
230
_("Top & Bottom Margins"),
232
self.add_frame_option(_("Page Options"),
233
_("Left & Right Margins"),
236
hpages_adj = gtk.Adjustment(value=1, lower=1, upper=25, step_incr=1)
237
vpages_adj = gtk.Adjustment(value=1, lower=1, upper=25, step_incr=1)
239
self.hpages_sb = gtk.SpinButton(adjustment=hpages_adj, digits=0)
240
self.vpages_sb = gtk.SpinButton(adjustment=vpages_adj, digits=0)
242
self.add_frame_option(_("Page Options"),
243
_("Number of Horizontal Pages"),
245
_("GraphViz can create very large graphs by "
246
"spreading the graph across a rectangular "
247
"array of pages. This controls the number "
248
"pages in the array horizontally."))
249
self.add_frame_option(_("Page Options"),
250
_("Number of Vertical Pages"),
252
_("GraphViz can create very large graphs "
253
"by spreading the graph across a "
254
"rectangular array of pages. This "
255
"controls the number pages in the array "
258
def toggle_date(self,obj):
259
if self.includedates_cb.get_active():
260
self.just_year_cb.set_sensitive(1)
262
self.just_year_cb.set_sensitive(0)
264
def make_doc_menu(self):
265
"""Build a one item menu of document types that are
266
appropriate for this report."""
267
name = "Graphviz (dot)"
268
menuitem = gtk.MenuItem (name)
269
menuitem.set_data ("d", name)
270
menuitem.set_data("paper",1)
271
if os.system ("dot </dev/null 2>/dev/null") == 0:
272
menuitem.set_data ("printable", _("Generate print output"))
275
myMenu.append (menuitem)
276
self.format_menu.set_menu(myMenu)
278
def make_document(self):
279
"""Do Nothing. This document will be created in the
280
make_report routine."""
283
def setup_style_frame(self):
284
"""The style frame is not used in this dialog."""
287
def parse_style_frame(self):
288
"""The style frame is not used in this dialog."""
291
def parse_other_frames(self):
292
menu = self.arrowstyle_optionmenu.get_menu()
293
self.arrowheadstyle, self.arrowtailstyle = menu.get_active().get_data('t')
294
self.includedates = self.includedates_cb.get_active()
295
self.includeurl = self.includeurl_cb.get_active()
296
self.tb_margin = self.tb_margin_sb.get_value()
297
self.lr_margin = self.lr_margin_sb.get_value()
298
self.colorize = self.colorize_cb.get_active()
299
self.adoptionsdashed = self.adoptionsdashed_cb.get_active()
300
self.hpages = self.hpages_sb.get_value_as_int()
301
self.vpages = self.vpages_sb.get_value_as_int()
302
self.show_families = self.show_families_cb.get_active()
303
self.just_year = self.just_year_cb.get_active()
305
menu = self.font_optionmenu.get_menu()
306
self.fontstyle = menu.get_active().get_data('t')
308
def make_report(self):
309
"""Create the object that will produce the GraphViz file."""
310
width = self.paper.get_width_inches()
311
height = self.paper.get_height_inches()
313
file = open(self.target_path,"w")
316
ind_list = self.filter.apply(self.db, self.db.getPersonMap().values())
317
except Errors.FilterError, msg:
318
from QuestionDialog import ErrorDialog
319
(m1,m2) = msg.messages()
322
write_dot(file, ind_list, self.orien, width, height,
323
self.tb_margin, self.lr_margin, self.hpages,
324
self.vpages, self.includedates, self.includeurl,
325
self.colorize, self.adoptionsdashed, self.arrowheadstyle,
326
self.arrowtailstyle, self.show_families, self.just_year,
329
if self.print_report.get_active ():
330
os.environ["DOT"] = self.target_path
331
os.system ('dot -Tps "$DOT" | %s &' %
332
Report.get_print_dialog_app ())
334
#------------------------------------------------------------------------
338
#------------------------------------------------------------------------
339
def report(database,person):
340
GraphVizDialog(database,person)
342
#------------------------------------------------------------------------
346
#------------------------------------------------------------------------
347
def write_dot(file, ind_list, orien, width, height, tb_margin,
348
lr_margin, hpages, vpages, includedates, includeurl,
349
colorize, adoptionsdashed, arrowheadstyle, arrowtailstyle,
350
show_families, just_year, fontstyle):
351
file.write("digraph g {\n")
352
file.write("bgcolor=white;\n")
353
file.write("rankdir=LR;\n")
354
file.write("center=1;\n")
355
file.write("margin=0.5;\n")
356
file.write("ratio=fill;\n")
357
file.write("size=\"%3.1f,%3.1f\";\n" % ((width*hpages)-(lr_margin*2)-((hpages-1)*1.0),
358
(height*vpages)-(tb_margin*2)-((vpages-1)*1.0)))
359
file.write("page=\"%3.1f,%3.1f\";\n" % (width,height))
361
if orien == BaseDoc.PAPER_LANDSCAPE:
362
file.write("rotate=90;\n")
364
if len(ind_list) > 1:
365
dump_index(ind_list,file,includedates,includeurl,colorize,
366
arrowheadstyle,arrowtailstyle,show_families,just_year,fontstyle)
367
dump_person(ind_list,file,adoptionsdashed,arrowheadstyle,
368
arrowtailstyle,show_families)
373
#------------------------------------------------------------------------
377
#------------------------------------------------------------------------
378
def dump_person(person_list,file,adoptionsdashed,arrowheadstyle,
379
arrowtailstyle,show_families):
380
# Hash people in a dictionary for faster inclusion checking.
382
for p in person_list:
383
person_dict[p.getId()] = 1
385
for person in person_list:
386
pid = string.replace(person.getId(),'-','_')
387
for family, mrel, frel in person.getParentList():
388
father = family.getFather()
389
mother = family.getMother()
390
fadopted = frel != _("Birth")
391
madopted = mrel != _("Birth")
392
if (show_families and
393
(father and person_dict.has_key(father.getId()) or
394
mother and person_dict.has_key(mother.getId()))):
395
# Link to the family node.
396
famid = string.replace(family.getId(),'-','_')
397
file.write('p%s -> f%s [' % (pid, famid))
398
file.write('arrowhead=%s, arrowtail=%s, ' %
399
(arrowheadstyle, arrowtailstyle))
400
if adoptionsdashed and (fadopted or madopted):
401
file.write('style=dashed')
403
file.write('style=solid')
406
# Link to the parents' nodes directly.
407
if father and person_dict.has_key(father.getId()):
408
fid = string.replace(father.getId(),'-','_')
409
file.write('p%s -> p%s [' % (pid, fid))
410
file.write('arrowhead=%s, arrowtail=%s, ' %
411
(arrowheadstyle, arrowtailstyle))
412
if adoptionsdashed and fadopted:
413
file.write('style=dashed')
415
file.write('style=solid')
417
if mother and person_dict.has_key(mother.getId()):
418
mid = string.replace(mother.getId(),'-','_')
419
file.write('p%s -> p%s [' % (pid, mid))
420
file.write('arrowhead=%s, arrowtail=%s, ' %
421
(arrowheadstyle, arrowtailstyle))
422
if adoptionsdashed and madopted:
423
file.write('style=dashed')
425
file.write('style=solid')
428
#------------------------------------------------------------------------
432
#------------------------------------------------------------------------
433
def dump_index(person_list,file,includedates,includeurl,colorize,
434
arrowheadstyle,arrowtailstyle,show_families,just_year,font):
435
# The list of families for which we have output the node, so we
438
for person in person_list:
439
# Output the person's node.
440
label = person.getPrimaryName().getName()
441
id = string.replace(person.getId(),'-','_')
443
if person.getBirth().getDateObj().getYearValid():
445
birth = '%i' % person.getBirth().getDateObj().getYear()
447
birth = person.getBirth().getDate()
450
if person.getDeath().getDateObj().getYearValid():
452
death = '%i' % person.getDeath().getDateObj().getYear()
454
death = person.getDeath().getDate()
457
label = label + '\\n(%s - %s)' % (birth, death)
458
file.write('p%s [shape=box, ' % id)
460
file.write('URL="%s.html", ' % id)
462
gender = person.getGender()
463
if gender == person.male:
464
file.write('color=dodgerblue4, ')
465
elif gender == person.female:
466
file.write('color=deeppink, ')
468
file.write('color=black, ')
470
file.write('fontname="%s", label="%s"];\n' % (font,label))
472
file.write('fontname="%s", label="%s"];\n' % (font,utf8_to_latin(label)))
473
# Output families's nodes.
475
family_list = person.getFamilyList()
476
for fam in family_list:
477
fid = string.replace(fam.getId(),'-','_')
478
if fam not in families_done:
479
families_done.append(fam)
480
file.write('f%s [shape=ellipse, ' % fid)
482
m = fam.getMarriage()
486
if do.getYearValid():
488
marriage = '%i' % do.getYear()
490
marriage = m.getDate()
491
file.write('fontname="%s", label="%s"];\n' % (font,marriage))
492
# Link this person to all his/her families.
493
file.write('f%s -> p%s [' % (fid, id))
494
file.write('arrowhead=%s, arrowtail=%s, ' %
495
(arrowheadstyle, arrowtailstyle))
496
file.write('style=solid];\n')
500
#------------------------------------------------------------------------
504
#------------------------------------------------------------------------
505
def get_description():
506
return _("Generates relationship graphs, currently only in GraphViz "
507
"format. GraphViz (dot) can transform the graph into "
508
"postscript, jpeg, png, vrml, svg, and many other formats. "
509
"For more information or to get a copy of GraphViz, "
510
"goto http://www.graphviz.org")
512
#------------------------------------------------------------------------
516
#------------------------------------------------------------------------
517
from Plugins import register_report
520
ver = sys.version_info
521
if ver[0] == 2 and ver[1] == 2:
524
_("Relationship Graph"),
526
category=_("Graphical Reports"),
527
description=get_description(),
528
author_name="Donald N. Allingham",
529
author_email="dallingham@users.sourceforge.net"