2
# Gramps - a GTK+/GNOME based genealogy program
4
# Copyright (C) 2000-2004 Donald N. Allingham
5
# Contributions by Lorenzo Cappelletti <lorenzo.cappelletti@email.it>
7
# This program is free software; you can redistribute it and/or modify
8
# it under the terms of the GNU General Public License as published by
9
# the Free Software Foundation; either version 2 of the License, or
10
# (at your option) any later version.
12
# This program is distributed in the hope that it will be useful,
13
# but WITHOUT ANY WARRANTY; without even the implied warranty of
14
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15
# GNU General Public License for more details.
17
# You should have received a copy of the GNU General Public License
18
# along with this program; if not, write to the Free Software
19
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
22
# $Id: RelGraph.py,v 1.3.2.1 2004/03/19 00:29:04 rshura Exp $
24
"Generate files/Relationship graph"
26
#------------------------------------------------------------------------
30
#------------------------------------------------------------------------
35
from time import asctime
37
#------------------------------------------------------------------------
41
#------------------------------------------------------------------------
44
#------------------------------------------------------------------------
48
#------------------------------------------------------------------------
55
from RelLib import Event
56
from gettext import gettext as _
57
from latin_utf8 import utf8_to_latin
59
#------------------------------------------------------------------------
63
#------------------------------------------------------------------------
64
_PS_FONT = 'Helvetica'
67
#------------------------------------------------------------------------
71
#------------------------------------------------------------------------
72
class RelGraphDialog(Report.ReportDialog):
74
# Default graph options
86
ArrowHeadStyle = 'none'
87
ArrowTailStyle = 'normal'
98
def __init__(self,database,person):
99
Report.ReportDialog.__init__(self,database,person,self.report_options)
102
"""The window title for this dialog"""
103
return "%s - %s - GRAMPS" % (_("Relationship Graph"),
104
_("Graphical Reports"))
106
def get_target_browser_title(self):
107
"""The title of the window created when the 'browse' button is
108
clicked in the 'Save As' frame."""
109
return _("Graphviz File")
111
def get_report_generations(self):
112
"""Default to 10 generations, no page breaks."""
115
def get_report_filters(self):
116
"""Set up the list of possible content filters."""
118
name = self.person.getPrimaryName().getName()
120
all = GenericFilter.GenericFilter()
121
all.set_name(_("Entire Database"))
122
all.add_rule(GenericFilter.Everyone([]))
124
des = GenericFilter.GenericFilter()
125
des.set_name(_("Descendants of %s") % name)
126
des.add_rule(GenericFilter.IsDescendantOf([self.person.getId()]))
128
fam = GenericFilter.GenericFilter()
129
fam.set_name(_("Descendant family members of %s") % name)
130
fam.add_rule(GenericFilter.IsDescendantFamilyOf([self.person.getId()]))
132
ans = GenericFilter.GenericFilter()
133
ans.set_name(_("Ancestors of %s") % name)
134
ans.add_rule(GenericFilter.IsAncestorOf([self.person.getId()]))
136
com = GenericFilter.GenericFilter()
137
com.set_name(_("People with common ancestor with %s") % name)
138
com.add_rule(GenericFilter.HasCommonAncestorWith([self.person.getId()]))
140
return [all, des, fam, ans, com]
142
def add_user_options(self):
143
self.arrowstyle_optionmenu = gtk.OptionMenu()
146
menuitem = gtk.MenuItem(_("Descendants <- Ancestors"))
147
menuitem.set_data('t', ('none', 'normal'))
149
menu.append(menuitem)
151
menuitem = gtk.MenuItem(_("Descendants -> Ancestors"))
152
menuitem.set_data('t', ('normal', 'none'))
154
menu.append(menuitem)
156
menuitem = gtk.MenuItem(_("Descendants <-> Ancestors"))
157
menuitem.set_data('t', ('normal', 'normal'))
159
menu.append(menuitem)
161
menuitem = gtk.MenuItem(_("Descendants - Ancestors"))
162
menuitem.set_data('t', ('none', 'none'))
164
menu.append(menuitem)
168
self.arrowstyle_optionmenu.set_menu(menu)
170
self.font_optionmenu = gtk.OptionMenu()
173
menuitem = gtk.MenuItem(_("TrueType"))
174
menuitem.set_data('t', _TT_FONT)
176
menu.append(menuitem)
178
menuitem = gtk.MenuItem(_("PostScript"))
179
menuitem.set_data('t', _PS_FONT)
181
menu.append(menuitem)
183
self.font_optionmenu.set_menu(menu)
185
self.add_frame_option(_("GraphViz Options"),
187
self.font_optionmenu,
188
_("Choose the font family."))
190
self.add_frame_option(_("GraphViz Options"),
191
_("Arrowhead Options"),
192
self.arrowstyle_optionmenu,
193
_("Choose the direction that the arrows point."))
196
self.show_as_stack_cb = gtk.CheckButton(_("Show family as a stack"))
197
self.show_as_stack_cb.set_active(self.ShowAsStack)
198
self.show_as_stack_cb.connect('toggled', self._grey_out_cb)
199
self.add_frame_option(_("GraphViz Options"), '',
200
self.show_as_stack_cb,
201
_("The main individual is shown along with "
202
"their spouses in a stack."))
204
self.show_families_cb = gtk.CheckButton(_("Show family nodes"))
205
self.show_families_cb.set_active(self.ShowFamilies)
206
self.show_families_cb.connect('toggled', self._grey_out_cb)
207
self.add_frame_option(_("GraphViz Options"), '',
208
self.show_families_cb,
209
_("Families will show up as ellipses, linked "
210
"to parents and children."))
211
msg = _("Include IDs")
212
self.includeid_cb = gtk.CheckButton(msg)
213
self.includeid_cb.set_active(self.IncludeId)
214
self.add_frame_option(_("GraphViz Options"), '',
216
_("Include individual and family IDs."))
218
msg = _("Include Birth, Marriage and Death Dates")
219
self.includedates_cb = gtk.CheckButton(msg)
220
self.includedates_cb.set_active(self.IncludeDates)
221
self.add_frame_option(_("GraphViz Options"), '',
222
self.includedates_cb,
223
_("Include the dates that the individual "
224
"was born, got married and/or died "
225
"in the graph labels."))
227
self.just_year_cb = gtk.CheckButton(_("Limit dates to years only"))
228
self.just_year_cb.set_active(self.JustYear)
229
self.add_frame_option(_("GraphViz Options"), '',
231
_("Prints just dates' year, neither "
232
"month or day nor date approximation "
233
"or interval are shown."))
235
self.place_cause_cb = gtk.CheckButton(_("Place/cause when no date"))
236
self.place_cause_cb.set_active(self.PlaceCause)
237
self.includedates_cb.connect('toggled', self._grey_out_cb)
238
self.add_frame_option(_("GraphViz Options"), '',
240
_("When no birth, marriage, or death date "
241
"is available, the correspondent place field "
242
"(or cause field when blank) will be used."))
244
self.includeurl_cb = gtk.CheckButton(_("Include URLs"))
245
self.includeurl_cb.set_active(self.IncludeUrl)
246
self.add_frame_option(_("GraphViz Options"), '',
248
_("Include a URL in each graph node so "
249
"that PDF and imagemap files can be "
250
"generated that contain active links "
251
"to the files generated by the 'Generate "
252
"Web Site' report."))
254
self.colorize_cb = gtk.CheckButton(_("Colorize Graph"))
255
self.colorize_cb.set_active(self.Colorize)
256
self.add_frame_option(_("GraphViz Options"),
259
_("Males will be outlined in blue, females "
260
"will be outlined in pink. If the sex of "
261
"an individual is unknown it will be "
262
"outlined in black."))
264
self.adoptionsdashed_cb = gtk.CheckButton(_("Indicate non-birth relationships with dashed lines"))
265
self.adoptionsdashed_cb.set_active(self.AdoptionsDashed)
266
self.add_frame_option(_("GraphViz Options"),
268
self.adoptionsdashed_cb,
269
_("Non-birth relationships will show up "
270
"as dashed lines in the graph."))
272
tb_margin_adj = gtk.Adjustment(value=0.5, lower=0.25,
273
upper=100.0, step_incr=0.25)
274
lr_margin_adj = gtk.Adjustment(value=0.5, lower=0.25,
275
upper=100.0, step_incr=0.25)
277
self.tb_margin_sb = gtk.SpinButton(adjustment=tb_margin_adj, digits=2)
278
self.lr_margin_sb = gtk.SpinButton(adjustment=lr_margin_adj, digits=2)
280
self.add_frame_option(_("Page Options"),
281
_("Top & Bottom Margins"),
283
self.add_frame_option(_("Page Options"),
284
_("Left & Right Margins"),
287
hpages_adj = gtk.Adjustment(value=1, lower=1, upper=25, step_incr=1)
288
vpages_adj = gtk.Adjustment(value=1, lower=1, upper=25, step_incr=1)
290
self.hpages_sb = gtk.SpinButton(adjustment=hpages_adj, digits=0)
291
self.vpages_sb = gtk.SpinButton(adjustment=vpages_adj, digits=0)
293
self.add_frame_option(_("Page Options"),
294
_("Number of Horizontal Pages"),
296
_("GraphViz can create very large graphs by "
297
"spreading the graph across a rectangular "
298
"array of pages. This controls the number "
299
"pages in the array horizontally."))
300
self.add_frame_option(_("Page Options"),
301
_("Number of Vertical Pages"),
303
_("GraphViz can create very large graphs "
304
"by spreading the graph across a "
305
"rectangular array of pages. This "
306
"controls the number pages in the array "
309
def _grey_out_cb (self, button):
310
if button == self.includedates_cb:
311
if button.get_active():
312
self.just_year_cb.set_sensitive(1)
313
self.place_cause_cb.set_sensitive(1)
315
self.just_year_cb.set_sensitive(0)
316
self.place_cause_cb.set_sensitive(0)
317
elif button == self.show_families_cb:
318
if button.get_active():
319
self.show_as_stack_cb.set_sensitive(0)
321
self.show_as_stack_cb.set_sensitive(1)
322
elif button == self.show_as_stack_cb:
323
if button.get_active():
324
self.show_families_cb.set_sensitive(0)
326
self.show_families_cb.set_sensitive(1)
328
def make_doc_menu(self):
329
"""Build a one item menu of document types that are
330
appropriate for this report."""
331
name = "Graphviz (dot)"
332
menuitem = gtk.MenuItem (name)
333
menuitem.set_data ("d", name)
334
menuitem.set_data("paper",1)
335
if os.system ("dot </dev/null 2>/dev/null") == 0:
336
menuitem.set_data ("printable", _("Generate print output"))
339
myMenu.append (menuitem)
340
self.format_menu.set_menu(myMenu)
342
def make_document(self):
343
"""Do Nothing. This document will be created in the
344
make_report routine."""
347
def setup_style_frame(self):
348
"""The style frame is not used in this dialog."""
351
def parse_style_frame(self):
352
"""The style frame is not used in this dialog."""
355
def parse_other_frames(self):
356
self.ShowAsStack = self.show_as_stack_cb.get_active()
357
self.ShowFamilies = self.show_families_cb.get_active()
358
self.IncludeDates = self.includedates_cb.get_active()
359
self.JustYear = self.just_year_cb.get_active()
360
self.PlaceCause = self.place_cause_cb.get_active()
361
self.IncludeId = self.includeid_cb.get_active()
362
self.IncludeUrl = self.includeurl_cb.get_active()
363
self.Colorize = self.colorize_cb.get_active()
365
self.font_optionmenu.get_menu().get_active().get_data('t')
366
self.ArrowHeadStyle, \
367
self.ArrowTailStyle =\
368
self.arrowstyle_optionmenu.get_menu().get_active().get_data('t')
369
self.AdoptionsDashed = self.adoptionsdashed_cb.get_active()
370
self.HPages = self.hpages_sb.get_value_as_int()
371
self.VPages = self.vpages_sb.get_value_as_int()
372
self.TBMargin = self.tb_margin_sb.get_value()
373
self.LRMargin = self.lr_margin_sb.get_value()
375
def make_report(self):
376
"""Create the object that will produce the GraphViz file."""
377
self.Width = self.paper.get_width_inches()
378
self.Height = self.paper.get_height_inches()
380
self.File = open(self.target_path,"w")
383
self.IndividualSet =\
384
Set(self.filter.apply(self.db, self.db.getPersonMap().values()))
385
self.IndividualSet.add(self.person)
386
except Errors.FilterError, msg:
387
from QuestionDialog import ErrorDialog
388
(m1,m2) = msg.messages()
393
if self.print_report.get_active ():
394
os.environ["DOT"] = self.target_path
395
os.system ('dot -Tps "$DOT" | %s &' %
396
Report.get_print_dialog_app ())
398
#------------------------------------------------------------------------
402
#------------------------------------------------------------------------
403
def report(database,person):
404
RelGraphDialog(database,person)
406
#------------------------------------------------------------------------
410
#------------------------------------------------------------------------
412
"""Write out to a file a relationship graph in dot format"""
413
self.File.write("/* GRAMPS - Relationship graph\n")
414
self.File.write(" *\n")
415
self.File.write(" * Report options:\n")
416
self.File.write(" * font style : %s\n" % self.FontStyle)
417
self.File.write(" * style arrow head : %s\n" % self.ArrowHeadStyle)
418
self.File.write(" * tail : %s\n" % self.ArrowTailStyle)
419
self.File.write(" * include URLs : %s\n" % self.IncludeUrl)
420
self.File.write(" * IDs : %s\n" % self.IncludeId)
421
self.File.write(" * dates : %s\n" % self.IncludeDates)
422
self.File.write(" * just year : %s\n" % self.JustYear)
423
self.File.write(" * place or cause : %s\n" % self.PlaceCause)
424
self.File.write(" * colorize : %s\n" % self.Colorize)
425
self.File.write(" * dashed adoptions : %s\n" % self.AdoptionsDashed)
426
self.File.write(" * show families : %s\n" % self.ShowFamilies)
427
self.File.write(" * as stack : %s\n" % self.ShowAsStack)
428
self.File.write(" * margins top/bottm : %s\n" % self.TBMargin)
429
self.File.write(" * left/right : %s\n" % self.LRMargin)
430
self.File.write(" * pages horizontal : %s\n" % self.HPages)
431
self.File.write(" * vertical : %s\n" % self.VPages)
432
self.File.write(" * page width : %sin\n" % self.Width)
433
self.File.write(" * height : %sin\n" % self.Height)
434
self.File.write(" *\n")
435
self.File.write(" * Generated on %s by GRAMPS\n" % asctime())
436
self.File.write(" */\n\n")
437
self.File.write("digraph GRAMPS_relationship_graph {\n")
438
self.File.write("bgcolor=white;\n")
439
self.File.write("rankdir=LR;\n")
440
self.File.write("center=1;\n")
441
self.File.write("margin=0.5;\n")
442
self.File.write("ratio=fill;\n")
443
self.File.write("size=\"%3.1f,%3.1f\";\n"
444
% ((self.Width*self.HPages) - (self.LRMargin*2) -
445
((self.HPages - 1)*1.0),
446
(self.Height*self.VPages) - (self.TBMargin*2) -
447
((self.VPages - 1)*1.0)))
448
self.File.write("page=\"%3.1f,%3.1f\";\n" % (self.Width, self.Height))
450
if len(self.IndividualSet) > 1:
452
_writeGraphRecord(self)
456
self.File.write("}\n// File end")
459
#------------------------------------------------------------------------
463
#------------------------------------------------------------------------
464
def _writeGraphBox (self):
465
"""Write out a graph body where all individuals are separated boxes"""
466
individualNodes = Set() # list of individual graph nodes
467
familyNodes = Set() # list of family graph nodes
468
# Writes out a not for each individual
469
self.File.write('\n// Individual nodes (box graph)\n')
470
_writeNode(self.File, shape='box', color='black', fontname=self.FontStyle)
471
for individual in self.IndividualSet:
472
individualNodes.add(individual)
473
individualId = _getIndividualId(individual)
474
(color, url) = _getIndividualData(self, individual)
475
label = _getIndividualLabel(self, individual)
476
_writeNode(self.File, individualId, label, color, url)
477
# Writes out a node for each family
478
if self.ShowFamilies:
479
self.File.write('\n// Family nodes (box graph)\n')
480
_writeNode(self.File, shape='ellipse', color='black',
481
fontname=self.FontStyle)
482
for individual in individualNodes:
483
for family in individual.getFamilyList():
484
if family not in familyNodes:
485
familyNodes.add(family)
486
familyId = _getFamilyId(family)
487
label = _getFamilyLabel(self, family)
488
_writeNode(self.File, familyId, label)
489
# Links each individual to their parents/family
490
self.File.write('\n// Individual edges\n')
491
_writeEdge(self.File, style="solid", arrowHead=self.ArrowHeadStyle,
492
arrowTail=self.ArrowTailStyle)
493
for individual in individualNodes:
494
individualId = _getIndividualId(individual)
495
for family, motherRelShip, fatherRelShip\
496
in individual.getParentList():
497
father = family.getFather()
498
mother = family.getMother()
499
if self.ShowFamilies and family in familyNodes:
500
# edge from an individual to their family
501
familyId = _getFamilyId(family)
502
style = _getEdgeStyle(self, fatherRelShip, motherRelShip)
503
_writeEdge(self.File, individualId, familyId, style)
505
# edge from an individual to their parents
506
if father and father in individualNodes:
507
fatherId = _getIndividualId(father)
508
_writeEdge(self.File, individualId, fatherId,
509
_getEdgeStyle(self, fatherRelShip))
510
if mother and mother in individualNodes:
511
motherId = _getIndividualId(mother)
512
_writeEdge(self.File, individualId, motherId,
513
_getEdgeStyle(self, motherRelShip))
514
# Links each family to its components
515
if self.ShowFamilies:
516
self.File.write('\n// Family edges (box graph)\n')
517
_writeEdge(self.File, style="solid", arrowHead=self.ArrowHeadStyle,
518
arrowTail=self.ArrowTailStyle)
519
for family in familyNodes:
520
familyId = _getFamilyId(family)
521
father = family.getFather()
522
if father and father in individualNodes:
523
fatherId = _getIndividualId(father)
524
_writeEdge(self.File, familyId, fatherId)
525
mother = family.getMother()
526
if mother and mother in individualNodes:
527
motherId = _getIndividualId(mother)
528
_writeEdge(self.File, familyId, motherId)
533
for individual in individualNodes:
534
if individual.getGender() == individual.male:
536
elif individual.getGender() == individual.female:
537
females = females + 1
539
unknowns = unknowns + 1
540
_writeStats(self.File, males, females, unknowns, len(familyNodes))
542
#------------------------------------------------------------------------
546
#------------------------------------------------------------------------
547
def _writeGraphRecord (self):
548
"""Write out a graph body where families are rendered as records"""
549
# Builds a dictionary of family records.
550
# Each record is made of an individual married with zero or
553
if isinstance(self.filter.get_rules()[0],
554
GenericFilter.IsDescendantFamilyOf):
555
# With the IsDescendantFamilyOf filter, the IndividualSet
556
# includes people which are not direct descendants of the
557
# active person (practically, spouses of direct
558
# discendants). Because we want the graph to highlight the
559
# consanguinity, IndividualSet is split in two subsets:
560
# naturalRelatives (direct descendants) and its complementary
561
# subset (in-law relatives).
562
filter = GenericFilter.GenericFilter()
563
filter.add_rule(GenericFilter.IsDescendantOf([self.person.getId()]))
565
Set(filter.apply(self.db, self.db.getPersonMap().values()))
566
naturalRelatives.add(self.person)
568
naturalRelatives = self.IndividualSet
569
self.File.write('\n// Family nodes (record graph)\n')
570
_writeNode(self.File, shape='record', color='black',
571
fontname=self.FontStyle)
572
for individual in naturalRelatives:
573
familyId = _getIndividualId(individual)
574
# If both husband and wife are members of the IndividualSet,
575
# only one record, with the husband first, is displayed.
576
if individual.getGender() == individual.female:
577
# There are exactly three cases where a female node is added:
578
family = None # no family
579
husbands = [] # filtered-in husbands (naturalRelatives)
580
unknownHusbands = 0 # filtered-out/unknown husbands
581
for family in individual.getFamilyList():
582
husband = family.getFather()
583
if husband and husband in self.IndividualSet:
584
if husband not in naturalRelatives:
585
husbands.append(husband)
588
if not family or len(husbands) or unknownHusbands:
589
familyNodes[familyId] = [individual] + husbands
591
familyNodes[familyId] = [individual]
592
for family in individual.getFamilyList():
593
wife = family.getMother()
594
if wife in self.IndividualSet:
595
familyNodes[familyId].append(wife)
596
# Writes out all family records
597
for familyId, family in familyNodes.items():
598
(color, url) = _getIndividualData(self, familyNodes[familyId][0])
599
label = _getFamilyRecordLabel(self, familyNodes[familyId])
600
_writeNode(self.File, familyId, label, color, url)
601
# Links individual's record to their parents' record
602
# The arrow goes from the individual port of a family record
603
# to the parent port of the parent's family record.
604
self.File.write('\n// Individual edges\n')
605
_writeEdge(self.File, style="solid", arrowHead=self.ArrowHeadStyle,
606
arrowTail=self.ArrowTailStyle)
607
for familyFromId, familyFrom in familyNodes.items():
608
for individualFrom in familyFrom:
609
individualFromId = _getIndividualId(individualFrom)
610
for family, motherRelShip, fatherRelShip\
611
in individualFrom.getParentList():
612
father = family.getFather()
613
mother = family.getMother()
614
# Things are complicated here because a parent may or
617
fatherId = _getIndividualId(father)
621
motherId = _getIndividualId(mother)
624
if familyNodes.has_key(fatherId):
625
if mother in familyNodes[fatherId]:
626
_writeEdge(self.File, familyFromId, fatherId,
627
_getEdgeStyle(self, motherRelShip),
628
portFrom=individualFromId, portTo=motherId)
630
_writeEdge(self.File, familyFromId, fatherId,
631
_getEdgeStyle(self, fatherRelShip),
632
portFrom=individualFromId)
633
if familyNodes.has_key(motherId):
634
if father in familyNodes[motherId]:
635
_writeEdge(self.File, familyFromId, motherId,
636
_getEdgeStyle(self, fatherRelShip),
637
portFrom=individualFromId, portTo=fatherId)
639
_writeEdge(self.File, familyFromId, motherId,
640
_getEdgeStyle(self, motherRelShip),
641
portFrom=individualFromId)
642
# Stats (unique individuals)
647
for familyId, family in familyNodes.items():
648
marriages = marriages + (len(family) - 1)
649
for individual in family:
650
if individual.getGender() == individual.male\
651
and individual not in males:
652
males.add(individual)
653
elif individual.getGender() == individual.female\
654
and individual not in females:
655
females.add(individual)
656
elif individual.getGender() == individual.unknown\
657
and individual not in unknowns:
658
unknowns.add(individual)
659
_writeStats(self.File, len(males), len(females), len(unknowns), marriages)
661
#------------------------------------------------------------------------
665
#------------------------------------------------------------------------
666
def _getIndividualId (individual):
667
"""Returns an individual id suitable for dot"""
668
return individual.getId()
670
#------------------------------------------------------------------------
674
#------------------------------------------------------------------------
675
def _getIndividualData (self, individual):
676
"""Returns a tuple of individual data"""
680
gender = individual.getGender()
681
if gender == individual.male:
682
color = 'dodgerblue4'
683
elif gender == individual.female:
688
url = "%s.html" % _getIndividualId(individual)
692
#------------------------------------------------------------------------
696
#------------------------------------------------------------------------
697
def _getEventLabel (self, event):
698
"""Returns a formatted string of event data suitable for a label"""
699
if self.IncludeDates and event:
700
dateObj = event.getDateObj()
701
if dateObj.getYearValid():
703
return "%i" % dateObj.getYear()
705
return dateObj.getDate()
706
elif self.PlaceCause:
707
if event.getPlaceName():
708
return event.getPlaceName()
710
return event.getCause()
713
#------------------------------------------------------------------------
715
# _getIndividualLabel
717
#------------------------------------------------------------------------
718
def _getIndividualLabel (self, individual, marriageEvent=None, family=None):
719
"""Returns a formatted string of individual data suitable for a label
721
Returned string always includes individual's name and optionally
722
individual's birth and death dates, individual's marriage date,
723
individual's and family's IDs.
726
individualId = individual.getId()
727
name = individual.getPrimaryName().getName()
728
if self.IncludeDates:
729
birth = _getEventLabel(self, individual.getBirth())
730
death = _getEventLabel(self, individual.getDeath())
731
if marriageEvent != None:
732
familyId = family.getId()
733
marriage = _getEventLabel(self, marriageEvent)
736
if marriageEvent != None:
737
label = "%s (%s)\\n" % (familyId, individualId)
739
label = "%s\\n" % individualId
743
if self.IncludeDates and (marriageEvent != None and marriage):
744
label = label + "%s\\n" % marriage
748
if self.IncludeDates and (birth or death):
749
label = label + "\\n%s - %s" % (birth, death)
752
#------------------------------------------------------------------------
756
#------------------------------------------------------------------------
757
def _getEdgeStyle (self, fatherRelShip, motherRelShip="Birth"):
758
"""Returns a edge style that depends on the relationships with parents"""
759
if self.AdoptionsDashed and \
760
(fatherRelShip != "Birth" or motherRelShip != "Birth"):
763
#------------------------------------------------------------------------
767
#------------------------------------------------------------------------
768
def _getFamilyId (family):
769
"""Returns a family id suitable for dot"""
770
return family.getId()
772
#------------------------------------------------------------------------
776
#------------------------------------------------------------------------
777
def _getFamilyLabel (self, family):
778
"""Returns a formatted string of family data suitable for a label"""
779
marriage = _getEventLabel(self, family.getMarriage())
781
return "%s\\n%s" % (family.getId(), marriage)
785
#------------------------------------------------------------------------
787
# _getFamilyRecordLabel
789
#------------------------------------------------------------------------
790
def _getFamilyRecordLabel (self, record):
791
"""Returns a formatted string of a family record suitable for a label"""
794
for individual in record:
795
individualId = _getIndividualId(individual)
796
if spouse == individual:
797
label = _getIndividualLabel(self, individual)
799
marriageEvent = Event()
800
for individualFamily in individual.getFamilyList():
801
if individualFamily in spouse.getFamilyList():
802
marriageEvent = individualFamily.getMarriage()
803
if not marriageEvent:
804
marriageEvent = Event()
806
label = _getIndividualLabel(self, individual, marriageEvent,
808
label = string.replace(label, "|", r"\|")
809
label = string.replace(label, "<", r"\<")
810
label = string.replace(label, ">", r"\>")
811
labels.append("<%s>%s" % (individualId, label))
812
return string.join(labels, "|")
814
#------------------------------------------------------------------------
818
#------------------------------------------------------------------------
819
def _writeNode (file, node="node", label="", color="", url="", shape="",
821
"""Writes out an individual node"""
822
file.write('%s [' % node)
824
if fontname == _TT_FONT:
825
file.write('label="%s" ' %
826
string.replace(label, '"', r'\"'))
828
file.write('label="%s" ' %
829
utf8_to_latin(string.replace(label, '"', r'\"')))
831
file.write('color=%s ' % color)
833
file.write('URL="%s" ' % string.replace(url, '"', r'\"'))
835
file.write('shape=%s ' % shape)
837
file.write('fontname="%s" ' % fontname)
840
#------------------------------------------------------------------------
844
#------------------------------------------------------------------------
845
def _writeEdge (file, nodeFrom="", nodeTo="", style="",
846
arrowHead="", arrowTail="", portFrom="", portTo=""):
847
"""Writes out an edge"""
848
if nodeFrom and nodeTo:
850
frm = nodeFrom + ":" + portFrom
854
to = nodeTo + ":" + portTo
857
file.write('%s -> %s [' % (frm, to))
859
file.write('edge [') # default edge
861
file.write('style=%s ' % style)
863
file.write('arrowhead=%s ' % arrowHead)
865
file.write('arrowtail=%s ' % arrowTail)
868
#------------------------------------------------------------------------
872
#------------------------------------------------------------------------
873
def _writeStats (file, males, females, unknowns, marriages):
874
file.write('\n/* Statistics\n')
875
file.write(' * individuals male : % 4d\n' % males)
876
file.write(' * female : % 4d\n' % females)
877
file.write(' * unknown : % 4d\n' % unknowns)
878
file.write(' * total : % 4d\n' % (males+females+unknowns))
879
file.write(' * marriages : % 4d\n' % marriages)
882
#------------------------------------------------------------------------
886
#------------------------------------------------------------------------
887
def get_description():
888
return _("Generates relationship graphs, currently only in GraphViz "
889
"format. GraphViz (dot) can transform the graph into "
890
"postscript, jpeg, png, vrml, svg, and many other formats. "
891
"For more information or to get a copy of GraphViz, "
892
"goto http://www.graphviz.org")
894
#------------------------------------------------------------------------
898
#------------------------------------------------------------------------
899
from Plugins import register_report
903
_("Relationship Graph"),
905
category=_("Graphical Reports"),
906
description=get_description(),
907
author_name="Donald N. Allingham",
908
author_email="dallingham@users.sourceforge.net"