~rosco2/ubuntu/wily/gramps/bug-1492304

« back to all changes in this revision

Viewing changes to src/plugins/RelGraph.py

  • Committer: Bazaar Package Importer
  • Author(s): James A. Treacy
  • Date: 2004-06-16 16:53:36 UTC
  • Revision ID: james.westby@ubuntu.com-20040616165336-kjzczqef4gnxrn2b
Tags: upstream-1.0.4
ImportĀ upstreamĀ versionĀ 1.0.4

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
#
 
2
# Gramps - a GTK+/GNOME based genealogy program
 
3
#
 
4
# Copyright (C) 2000-2004  Donald N. Allingham
 
5
# Contributions by Lorenzo Cappelletti <lorenzo.cappelletti@email.it>
 
6
#
 
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.
 
11
#
 
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.
 
16
#
 
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
 
20
#
 
21
 
 
22
# $Id: RelGraph.py,v 1.3.2.1 2004/03/19 00:29:04 rshura Exp $
 
23
 
 
24
"Generate files/Relationship graph"
 
25
 
 
26
#------------------------------------------------------------------------
 
27
#
 
28
# python modules
 
29
#
 
30
#------------------------------------------------------------------------
 
31
import os
 
32
import string
 
33
 
 
34
from sets import Set
 
35
from time import asctime
 
36
 
 
37
#------------------------------------------------------------------------
 
38
#
 
39
# GNOME/gtk
 
40
#
 
41
#------------------------------------------------------------------------
 
42
import gtk
 
43
 
 
44
#------------------------------------------------------------------------
 
45
#
 
46
# GRAMPS modules
 
47
#
 
48
#------------------------------------------------------------------------
 
49
import Utils
 
50
import Report
 
51
import BaseDoc
 
52
import GenericFilter
 
53
import Errors
 
54
 
 
55
from RelLib import Event
 
56
from gettext import gettext as _
 
57
from latin_utf8 import utf8_to_latin
 
58
 
 
59
#------------------------------------------------------------------------
 
60
#
 
61
# constants
 
62
#
 
63
#------------------------------------------------------------------------
 
64
_PS_FONT = 'Helvetica'
 
65
_TT_FONT = 'FreeSans'
 
66
 
 
67
#------------------------------------------------------------------------
 
68
#
 
69
# RelGraphDialog
 
70
#
 
71
#------------------------------------------------------------------------
 
72
class RelGraphDialog(Report.ReportDialog):
 
73
 
 
74
    # Default graph options
 
75
    File            = None
 
76
    IndividualSet   = Set()
 
77
    ShowAsStack     = 0
 
78
    ShowFamilies    = 0
 
79
    IncludeDates    = 1
 
80
    JustYear        = 0
 
81
    PlaceCause      = 1
 
82
    IncludeUrl      = 1
 
83
    IncludeId       = 0
 
84
    Colorize        = 1
 
85
    FontStyle       = _TT_FONT
 
86
    ArrowHeadStyle  = 'none'
 
87
    ArrowTailStyle  = 'normal'
 
88
    AdoptionsDashed = 1
 
89
    Width           = 0
 
90
    Height          = 0
 
91
    HPages          = 1
 
92
    VPages          = 1
 
93
    TBMargin        = 0
 
94
    LRMargin        = 0
 
95
 
 
96
    report_options = {}
 
97
 
 
98
    def __init__(self,database,person):
 
99
        Report.ReportDialog.__init__(self,database,person,self.report_options)
 
100
 
 
101
    def get_title(self):
 
102
        """The window title for this dialog"""
 
103
        return "%s - %s - GRAMPS" % (_("Relationship Graph"),
 
104
                                     _("Graphical Reports"))
 
105
 
 
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")
 
110
 
 
111
    def get_report_generations(self):
 
112
        """Default to 10 generations, no page breaks."""
 
113
        return (10, 0)
 
114
 
 
115
    def get_report_filters(self):
 
116
        """Set up the list of possible content filters."""
 
117
 
 
118
        name = self.person.getPrimaryName().getName()
 
119
 
 
120
        all = GenericFilter.GenericFilter()
 
121
        all.set_name(_("Entire Database"))
 
122
        all.add_rule(GenericFilter.Everyone([]))
 
123
 
 
124
        des = GenericFilter.GenericFilter()
 
125
        des.set_name(_("Descendants of %s") % name)
 
126
        des.add_rule(GenericFilter.IsDescendantOf([self.person.getId()]))
 
127
 
 
128
        fam = GenericFilter.GenericFilter()
 
129
        fam.set_name(_("Descendant family members of %s") % name)
 
130
        fam.add_rule(GenericFilter.IsDescendantFamilyOf([self.person.getId()]))
 
131
 
 
132
        ans = GenericFilter.GenericFilter()
 
133
        ans.set_name(_("Ancestors of %s") % name)
 
134
        ans.add_rule(GenericFilter.IsAncestorOf([self.person.getId()]))
 
135
 
 
136
        com = GenericFilter.GenericFilter()
 
137
        com.set_name(_("People with common ancestor with %s") % name)
 
138
        com.add_rule(GenericFilter.HasCommonAncestorWith([self.person.getId()]))
 
139
 
 
140
        return [all, des, fam, ans, com]
 
141
 
 
142
    def add_user_options(self):
 
143
        self.arrowstyle_optionmenu = gtk.OptionMenu()
 
144
        menu = gtk.Menu()
 
145
 
 
146
        menuitem = gtk.MenuItem(_("Descendants <- Ancestors"))
 
147
        menuitem.set_data('t', ('none', 'normal'))
 
148
        menuitem.show()
 
149
        menu.append(menuitem)
 
150
 
 
151
        menuitem = gtk.MenuItem(_("Descendants -> Ancestors"))
 
152
        menuitem.set_data('t', ('normal', 'none'))
 
153
        menuitem.show()
 
154
        menu.append(menuitem)
 
155
 
 
156
        menuitem = gtk.MenuItem(_("Descendants <-> Ancestors"))
 
157
        menuitem.set_data('t', ('normal', 'normal'))
 
158
        menuitem.show()
 
159
        menu.append(menuitem)
 
160
 
 
161
        menuitem = gtk.MenuItem(_("Descendants - Ancestors"))
 
162
        menuitem.set_data('t', ('none', 'none'))
 
163
        menuitem.show()
 
164
        menu.append(menuitem)
 
165
 
 
166
        menu.set_active(0)
 
167
 
 
168
        self.arrowstyle_optionmenu.set_menu(menu)
 
169
 
 
170
        self.font_optionmenu = gtk.OptionMenu()
 
171
        menu = gtk.Menu()
 
172
 
 
173
        menuitem = gtk.MenuItem(_("TrueType"))
 
174
        menuitem.set_data('t', _TT_FONT)
 
175
        menuitem.show()
 
176
        menu.append(menuitem)
 
177
 
 
178
        menuitem = gtk.MenuItem(_("PostScript"))
 
179
        menuitem.set_data('t', _PS_FONT)
 
180
        menuitem.show()
 
181
        menu.append(menuitem)
 
182
 
 
183
        self.font_optionmenu.set_menu(menu)
 
184
 
 
185
        self.add_frame_option(_("GraphViz Options"),
 
186
                              _("Font Options"),
 
187
                              self.font_optionmenu,
 
188
                              _("Choose the font family."))
 
189
 
 
190
        self.add_frame_option(_("GraphViz Options"),
 
191
                              _("Arrowhead Options"),
 
192
                              self.arrowstyle_optionmenu,
 
193
                              _("Choose the direction that the arrows point."))
 
194
 
 
195
 
 
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."))
 
203
 
 
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"), '',
 
215
                              self.includeid_cb,
 
216
                              _("Include individual and family IDs."))
 
217
 
 
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."))
 
226
 
 
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"), '',
 
230
                              self.just_year_cb,
 
231
                              _("Prints just dates' year, neither "
 
232
                                "month or day nor date approximation "
 
233
                                "or interval are shown."))
 
234
 
 
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"), '',
 
239
                              self.place_cause_cb,
 
240
                              _("When no birth, marriage, or death date "
 
241
                                "is available, the correspondent place field "
 
242
                                "(or cause field when blank) will be used."))
 
243
 
 
244
        self.includeurl_cb = gtk.CheckButton(_("Include URLs"))
 
245
        self.includeurl_cb.set_active(self.IncludeUrl)
 
246
        self.add_frame_option(_("GraphViz Options"), '',
 
247
                              self.includeurl_cb,
 
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."))
 
253
 
 
254
        self.colorize_cb = gtk.CheckButton(_("Colorize Graph"))
 
255
        self.colorize_cb.set_active(self.Colorize)
 
256
        self.add_frame_option(_("GraphViz Options"),
 
257
                              '',
 
258
                              self.colorize_cb,
 
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."))
 
263
 
 
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"),
 
267
                              '',
 
268
                              self.adoptionsdashed_cb,
 
269
                              _("Non-birth relationships will show up "
 
270
                                "as dashed lines in the graph."))
 
271
 
 
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)
 
276
 
 
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)
 
279
 
 
280
        self.add_frame_option(_("Page Options"),
 
281
                              _("Top & Bottom Margins"),
 
282
                              self.tb_margin_sb)
 
283
        self.add_frame_option(_("Page Options"),
 
284
                              _("Left & Right Margins"),
 
285
                              self.lr_margin_sb)
 
286
 
 
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)
 
289
 
 
290
        self.hpages_sb = gtk.SpinButton(adjustment=hpages_adj, digits=0)
 
291
        self.vpages_sb = gtk.SpinButton(adjustment=vpages_adj, digits=0)
 
292
 
 
293
        self.add_frame_option(_("Page Options"),
 
294
                              _("Number of Horizontal Pages"),
 
295
                              self.hpages_sb,
 
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"),
 
302
                              self.vpages_sb,
 
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 "
 
307
                                "vertically."))
 
308
 
 
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)
 
314
            else:
 
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)
 
320
            else:
 
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)
 
325
            else:
 
326
                self.show_families_cb.set_sensitive(1)
 
327
 
 
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"))
 
337
        menuitem.show ()
 
338
        myMenu = gtk.Menu ()
 
339
        myMenu.append (menuitem)
 
340
        self.format_menu.set_menu(myMenu)
 
341
 
 
342
    def make_document(self):
 
343
        """Do Nothing.  This document will be created in the
 
344
        make_report routine."""
 
345
        pass
 
346
 
 
347
    def setup_style_frame(self):
 
348
        """The style frame is not used in this dialog."""
 
349
        pass
 
350
 
 
351
    def parse_style_frame(self):
 
352
        """The style frame is not used in this dialog."""
 
353
        pass
 
354
 
 
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()
 
364
        self.FontStyle =\
 
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()
 
374
 
 
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()
 
379
 
 
380
        self.File = open(self.target_path,"w")
 
381
 
 
382
        try:
 
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()
 
389
            ErrorDialog(m1,m2)
 
390
 
 
391
        _writeDot(self)
 
392
 
 
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 ())
 
397
 
 
398
#------------------------------------------------------------------------
 
399
#
 
400
#
 
401
#
 
402
#------------------------------------------------------------------------
 
403
def report(database,person):
 
404
    RelGraphDialog(database,person)
 
405
 
 
406
#------------------------------------------------------------------------
 
407
#
 
408
# _writeDot
 
409
#
 
410
#------------------------------------------------------------------------
 
411
def _writeDot(self):
 
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))
 
449
 
 
450
    if len(self.IndividualSet) > 1:
 
451
        if self.ShowAsStack:
 
452
            _writeGraphRecord(self)
 
453
        else:
 
454
            _writeGraphBox(self)
 
455
 
 
456
    self.File.write("}\n// File end")
 
457
    self.File.close()
 
458
 
 
459
#------------------------------------------------------------------------
 
460
#
 
461
# _writeGraphBox
 
462
#
 
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)
 
504
            else:
 
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)
 
529
    # Statistics
 
530
    males = 0
 
531
    females = 0
 
532
    unknowns = 0
 
533
    for individual in individualNodes:
 
534
        if individual.getGender() == individual.male:
 
535
            males = males + 1
 
536
        elif individual.getGender() == individual.female:
 
537
            females = females + 1
 
538
        else:
 
539
            unknowns = unknowns + 1
 
540
    _writeStats(self.File, males, females, unknowns, len(familyNodes))
 
541
 
 
542
#------------------------------------------------------------------------
 
543
#
 
544
# _writeGraphRecord
 
545
#
 
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
 
551
    # more individuals.
 
552
    familyNodes = {}
 
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()]))
 
564
        naturalRelatives =\
 
565
            Set(filter.apply(self.db, self.db.getPersonMap().values()))
 
566
        naturalRelatives.add(self.person)
 
567
    else:
 
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)
 
586
                else:
 
587
                    unknownHusbands = 1
 
588
            if not family or len(husbands) or unknownHusbands:
 
589
                familyNodes[familyId] = [individual] + husbands
 
590
        else:
 
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
 
615
                # or may not exist.
 
616
                if father:
 
617
                    fatherId = _getIndividualId(father)
 
618
                else:
 
619
                    fatherId = ""
 
620
                if mother:
 
621
                    motherId = _getIndividualId(mother)
 
622
                else:
 
623
                    motherId = ""
 
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)
 
629
                    else:
 
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)
 
638
                    else:
 
639
                        _writeEdge(self.File, familyFromId, motherId,
 
640
                                   _getEdgeStyle(self, motherRelShip),
 
641
                                   portFrom=individualFromId)
 
642
    # Stats (unique individuals)
 
643
    males = Set()
 
644
    females = Set()
 
645
    unknowns = Set()
 
646
    marriages = 0
 
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)
 
660
 
 
661
#------------------------------------------------------------------------
 
662
#
 
663
# _getIndividualId
 
664
#
 
665
#------------------------------------------------------------------------
 
666
def _getIndividualId (individual):
 
667
    """Returns an individual id suitable for dot"""
 
668
    return individual.getId()
 
669
 
 
670
#------------------------------------------------------------------------
 
671
#
 
672
# _getIndividualData
 
673
#
 
674
#------------------------------------------------------------------------
 
675
def _getIndividualData (self, individual):
 
676
    """Returns a tuple of individual data"""
 
677
    # color
 
678
    color = ''
 
679
    if self.Colorize:
 
680
        gender = individual.getGender()
 
681
        if gender == individual.male:
 
682
            color = 'dodgerblue4'
 
683
        elif gender == individual.female:
 
684
            color  = 'deeppink'
 
685
    # url
 
686
    url = ''
 
687
    if self.IncludeUrl:
 
688
        url = "%s.html" % _getIndividualId(individual)
 
689
 
 
690
    return (color, url)
 
691
 
 
692
#------------------------------------------------------------------------
 
693
#
 
694
# _getEventLabel
 
695
#
 
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():
 
702
            if self.JustYear:
 
703
                return "%i" % dateObj.getYear()
 
704
            else:
 
705
                return dateObj.getDate()
 
706
        elif self.PlaceCause:
 
707
            if event.getPlaceName():
 
708
                return event.getPlaceName()
 
709
            else:
 
710
                return event.getCause()
 
711
    return ''
 
712
 
 
713
#------------------------------------------------------------------------
 
714
#
 
715
# _getIndividualLabel
 
716
#
 
717
#------------------------------------------------------------------------
 
718
def _getIndividualLabel (self, individual, marriageEvent=None, family=None):
 
719
    """Returns a formatted string of individual data suitable for a label
 
720
 
 
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.
 
724
    """
 
725
    # Get data ready
 
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)
 
734
    # Id
 
735
    if self.IncludeId:
 
736
        if marriageEvent != None:
 
737
            label = "%s (%s)\\n" % (familyId, individualId)
 
738
        else:
 
739
            label = "%s\\n" % individualId
 
740
    else:
 
741
        label = ""
 
742
    # Marriage date
 
743
    if self.IncludeDates and (marriageEvent != None and marriage):
 
744
        label = label + "%s\\n" % marriage
 
745
    # Individual's name
 
746
    label = label + name
 
747
    # Birth and death
 
748
    if self.IncludeDates and (birth or death):
 
749
        label = label + "\\n%s - %s" % (birth, death)
 
750
    return label
 
751
 
 
752
#------------------------------------------------------------------------
 
753
#
 
754
# _getEdgeStyle
 
755
#
 
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"):
 
761
        return "dashed"
 
762
 
 
763
#------------------------------------------------------------------------
 
764
#
 
765
# _getFamilyId
 
766
#
 
767
#------------------------------------------------------------------------
 
768
def _getFamilyId (family):
 
769
    """Returns a family id suitable for dot"""
 
770
    return family.getId()
 
771
 
 
772
#------------------------------------------------------------------------
 
773
#
 
774
# _getFamilyLabel
 
775
#
 
776
#------------------------------------------------------------------------
 
777
def _getFamilyLabel (self, family):
 
778
    """Returns a formatted string of family data suitable for a label"""
 
779
    marriage = _getEventLabel(self, family.getMarriage())
 
780
    if self.IncludeId:
 
781
        return "%s\\n%s" % (family.getId(), marriage)
 
782
    else:
 
783
        return marriage
 
784
 
 
785
#------------------------------------------------------------------------
 
786
#
 
787
# _getFamilyRecordLabel
 
788
#
 
789
#------------------------------------------------------------------------
 
790
def _getFamilyRecordLabel (self, record):
 
791
    """Returns a formatted string of a family record suitable for a label"""
 
792
    labels = []
 
793
    spouse = record[0]
 
794
    for individual in record:
 
795
        individualId = _getIndividualId(individual)
 
796
        if spouse == individual:
 
797
            label = _getIndividualLabel(self, individual)
 
798
        else:
 
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()
 
805
                    break
 
806
            label = _getIndividualLabel(self, individual, marriageEvent,
 
807
                                        individualFamily)
 
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, "|")
 
813
 
 
814
#------------------------------------------------------------------------
 
815
#
 
816
# _writeNode
 
817
#
 
818
#------------------------------------------------------------------------
 
819
def _writeNode (file, node="node", label="", color="", url="", shape="",
 
820
               fontname=""):
 
821
    """Writes out an individual node"""
 
822
    file.write('%s [' % node)
 
823
    if label:
 
824
        if fontname == _TT_FONT:
 
825
            file.write('label="%s" ' %
 
826
                   string.replace(label, '"', r'\"'))
 
827
        else:
 
828
            file.write('label="%s" ' %
 
829
                   utf8_to_latin(string.replace(label, '"', r'\"')))
 
830
    if color:
 
831
        file.write('color=%s ' % color)
 
832
    if url:
 
833
        file.write('URL="%s" ' % string.replace(url, '"', r'\"'))
 
834
    if shape:
 
835
        file.write('shape=%s ' % shape)
 
836
    if fontname:
 
837
        file.write('fontname="%s" ' % fontname)
 
838
    file.write('];\n')
 
839
 
 
840
#------------------------------------------------------------------------
 
841
#
 
842
# _writeEdge
 
843
#
 
844
#------------------------------------------------------------------------
 
845
def _writeEdge (file, nodeFrom="", nodeTo="", style="",
 
846
               arrowHead="", arrowTail="", portFrom="", portTo=""):
 
847
    """Writes out an edge"""
 
848
    if nodeFrom and nodeTo:
 
849
        if portFrom:
 
850
            frm = nodeFrom + ":" + portFrom
 
851
        else:
 
852
            frm = nodeFrom
 
853
        if portTo:
 
854
            to = nodeTo + ":" + portTo
 
855
        else:
 
856
            to = nodeTo
 
857
        file.write('%s -> %s [' % (frm, to))
 
858
    else:
 
859
        file.write('edge [')  # default edge
 
860
    if style:
 
861
        file.write('style=%s ' % style)
 
862
    if arrowHead:
 
863
        file.write('arrowhead=%s ' % arrowHead)
 
864
    if arrowTail:
 
865
        file.write('arrowtail=%s ' % arrowTail)
 
866
    file.write('];\n')
 
867
 
 
868
#------------------------------------------------------------------------
 
869
#
 
870
# _writeStats
 
871
#
 
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)
 
880
    file.write(' */\n')
 
881
 
 
882
#------------------------------------------------------------------------
 
883
#
 
884
#
 
885
#
 
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")
 
893
 
 
894
#------------------------------------------------------------------------
 
895
#
 
896
#
 
897
#
 
898
#------------------------------------------------------------------------
 
899
from Plugins import register_report
 
900
 
 
901
register_report(
 
902
    report,
 
903
    _("Relationship Graph"),
 
904
    status=(_("Beta")),
 
905
    category=_("Graphical Reports"),
 
906
    description=get_description(),
 
907
    author_name="Donald N. Allingham",
 
908
    author_email="dallingham@users.sourceforge.net"
 
909
    )