1#
2# Gramps - a GTK+/GNOME based genealogy program
3#
4# Copyright (C) 2007-2008  Stephane Charette
5# Copyright (C) 2007-2008  Brian G. Matherly
6# Copyright (C) 2009-2010  Gary Burton
7# Contribution 2009 by     Bob Ham <rah@bash.sh>
8# Copyright (C) 2010       Jakim Friant
9# Copyright (C) 2011-2014  Paul Franklin
10#
11# This program is free software; you can redistribute it and/or modify
12# it under the terms of the GNU General Public License as published by
13# the Free Software Foundation; either version 2 of the License, or
14# (at your option) any later version.
15#
16# This program is distributed in the hope that it will be useful,
17# but WITHOUT ANY WARRANTY; without even the implied warranty of
18# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
19# GNU General Public License for more details.
20#
21# You should have received a copy of the GNU General Public License
22# along with this program; if not, write to the Free Software
23# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
24#
25
26"""
27Family Lines, a Graphviz-based plugin for Gramps.
28"""
29
30#------------------------------------------------------------------------
31#
32# python modules
33#
34#------------------------------------------------------------------------
35from functools import partial
36import html
37
38#------------------------------------------------------------------------
39#
40# Set up logging
41#
42#------------------------------------------------------------------------
43import logging
44LOG = logging.getLogger(".FamilyLines")
45
46#------------------------------------------------------------------------
47#
48# Gramps module
49#
50#------------------------------------------------------------------------
51from gramps.gen.const import GRAMPS_LOCALE as glocale
52_ = glocale.translation.gettext
53from gramps.gen.lib import EventRoleType, EventType, Person, PlaceType, Date
54from gramps.gen.utils.file import media_path_full
55from gramps.gen.utils.thumbnails import (get_thumbnail_path, SIZE_NORMAL,
56                                         SIZE_LARGE)
57from gramps.gen.plug.report import Report
58from gramps.gen.plug.report import utils
59from gramps.gen.plug.report import MenuReportOptions
60from gramps.gen.plug.report import stdoptions
61from gramps.gen.plug.menu import (NumberOption, ColorOption, BooleanOption,
62                                  EnumeratedListOption, PersonListOption,
63                                  SurnameColorOption)
64from gramps.gen.utils.db import get_birth_or_fallback, get_death_or_fallback
65from gramps.gen.proxy import CacheProxyDb
66from gramps.gen.errors import ReportError
67from gramps.gen.display.place import displayer as _pd
68
69#------------------------------------------------------------------------
70#
71# Constant options items
72#
73#------------------------------------------------------------------------
74_COLORS = [{'name' : _("B&W outline"), 'value' : "outline"},
75           {'name' : _("Colored outline"), 'value' : "colored"},
76           {'name' : _("Color fill"), 'value' : "filled"}]
77
78_ARROWS = [ { 'name' : _("Descendants <- Ancestors"),  'value' : 'd' },
79            { 'name' : _("Descendants -> Ancestors"),  'value' : 'a' },
80            { 'name' : _("Descendants <-> Ancestors"), 'value' : 'da' },
81            { 'name' : _("Descendants - Ancestors"),   'value' : '' }]
82
83_CORNERS = [ { 'name' : _("None"),  'value' : '' },
84             { 'name' : _("Female"), 'value' : 'f' },
85             { 'name' : _("Male"),   'value' : 'm' },
86             { 'name' : _("Both"),  'value' : 'fm' }]
87
88#------------------------------------------------------------------------
89#
90# A quick overview of the classes we'll be using:
91#
92#   class FamilyLinesOptions(MenuReportOptions)
93#       - this class is created when the report dialog comes up
94#       - all configuration controls for the report are created here
95#
96#   class FamilyLinesReport(Report)
97#       - this class is created only after the user clicks on "OK"
98#       - the actual report generation is done by this class
99#
100#------------------------------------------------------------------------
101
102class FamilyLinesOptions(MenuReportOptions):
103    """
104    Defines all of the controls necessary
105    to configure the FamilyLines report.
106    """
107    def __init__(self, name, dbase):
108        self.limit_parents = None
109        self.max_parents = None
110        self.limit_children = None
111        self.max_children = None
112        self.include_images = None
113        self.image_location = None
114        self.justyears = None
115        self.include_dates = None
116        MenuReportOptions.__init__(self, name, dbase)
117
118    def add_menu_options(self, menu):
119
120        # ---------------------
121        category_name = _('Report Options')
122        add_option = partial(menu.add_option, category_name)
123        # ---------------------
124
125        followpar = BooleanOption(_('Follow parents to determine '
126                                    '"family lines"'), True)
127        followpar.set_help(_('Parents and their ancestors will be '
128                             'considered when determining "family lines".'))
129        add_option('followpar', followpar)
130
131        followchild = BooleanOption(_('Follow children to determine '
132                                      '"family lines"'), True)
133        followchild.set_help(_('Children will be considered when '
134                               'determining "family lines".'))
135        add_option('followchild', followchild)
136
137        remove_extra_people = BooleanOption(_('Try to remove extra '
138                                              'people and families'), True)
139        remove_extra_people.set_help(_('People and families not directly '
140                                       'related to people of interest will '
141                                       'be removed when determining '
142                                       '"family lines".'))
143        add_option('removeextra', remove_extra_people)
144
145        arrow = EnumeratedListOption(_("Arrowhead direction"), 'd')
146        for i in range( 0, len(_ARROWS) ):
147            arrow.add_item(_ARROWS[i]["value"], _ARROWS[i]["name"])
148        arrow.set_help(_("Choose the direction that the arrows point."))
149        add_option("arrow", arrow)
150
151        color = EnumeratedListOption(_("Graph coloring"), "filled")
152        for i in range(len(_COLORS)):
153            color.add_item(_COLORS[i]["value"], _COLORS[i]["name"])
154        color.set_help(_("Males will be shown with blue, females "
155                         "with red, unless otherwise set above for filled. "
156                         "If the sex of an individual "
157                         "is unknown it will be shown with gray."))
158        add_option("color", color)
159
160        roundedcorners = EnumeratedListOption(_("Rounded corners"), '')
161        for i in range( 0, len(_CORNERS) ):
162            roundedcorners.add_item(_CORNERS[i]["value"], _CORNERS[i]["name"])
163        roundedcorners.set_help(_("Use rounded corners e.g. to differentiate "
164                         "between women and men."))
165        add_option("useroundedcorners", roundedcorners)
166
167        stdoptions.add_gramps_id_option(menu, category_name, ownline=True)
168
169        # ---------------------
170        category_name = _('Report Options (2)')
171        add_option = partial(menu.add_option, category_name)
172        # ---------------------
173
174        stdoptions.add_name_format_option(menu, category_name)
175
176        stdoptions.add_private_data_option(menu, category_name, default=False)
177
178        stdoptions.add_living_people_option(menu, category_name)
179
180        locale_opt = stdoptions.add_localization_option(menu, category_name)
181
182        stdoptions.add_date_format_option(menu, category_name, locale_opt)
183
184        # --------------------------------
185        add_option = partial(menu.add_option, _('People of Interest'))
186        # --------------------------------
187
188        person_list = PersonListOption(_('People of interest'))
189        person_list.set_help(_('People of interest are used as a starting '
190                               'point when determining "family lines".'))
191        add_option('gidlist', person_list)
192
193        self.limit_parents = BooleanOption(_('Limit the number of ancestors'),
194                                           False)
195        self.limit_parents.set_help(_('Whether to '
196                                      'limit the number of ancestors.'))
197        add_option('limitparents', self.limit_parents)
198        self.limit_parents.connect('value-changed', self.limit_changed)
199
200        self.max_parents = NumberOption('', 50, 10, 9999)
201        self.max_parents.set_help(_('The maximum number '
202                                    'of ancestors to include.'))
203        add_option('maxparents', self.max_parents)
204
205        self.limit_children = BooleanOption(_('Limit the number '
206                                              'of descendants'),
207                                            False)
208        self.limit_children.set_help(_('Whether to '
209                                       'limit the number of descendants.'))
210        add_option('limitchildren', self.limit_children)
211        self.limit_children.connect('value-changed', self.limit_changed)
212
213        self.max_children = NumberOption('', 50, 10, 9999)
214        self.max_children.set_help(_('The maximum number '
215                                     'of descendants to include.'))
216        add_option('maxchildren', self.max_children)
217
218        # --------------------
219        category_name = _('Include')
220        add_option = partial(menu.add_option, category_name)
221        # --------------------
222
223        self.include_dates = BooleanOption(_('Include dates'), True)
224        self.include_dates.set_help(_('Whether to include dates for people '
225                                      'and families.'))
226        add_option('incdates', self.include_dates)
227        self.include_dates.connect('value-changed', self.include_dates_changed)
228
229        self.justyears = BooleanOption(_("Limit dates to years only"), False)
230        self.justyears.set_help(_("Prints just dates' year, neither "
231                                  "month or day nor date approximation "
232                                  "or interval are shown."))
233        add_option("justyears", self.justyears)
234
235        include_places = BooleanOption(_('Include places'), True)
236        include_places.set_help(_('Whether to include placenames for people '
237                                  'and families.'))
238        add_option('incplaces', include_places)
239
240        include_num_children = BooleanOption(_('Include the number of '
241                                               'children'), True)
242        include_num_children.set_help(_('Whether to include the number of '
243                                        'children for families with more '
244                                        'than 1 child.'))
245        add_option('incchildcnt', include_num_children)
246
247        self.include_images = BooleanOption(_('Include '
248                                              'thumbnail images of people'),
249                                            True)
250        self.include_images.set_help(_('Whether to '
251                                       'include thumbnail images of people.'))
252        add_option('incimages', self.include_images)
253        self.include_images.connect('value-changed', self.images_changed)
254
255        self.image_location = EnumeratedListOption(_('Thumbnail location'), 0)
256        self.image_location.add_item(0, _('Above the name'))
257        self.image_location.add_item(1, _('Beside the name'))
258        self.image_location.set_help(_('Where the thumbnail image '
259                                       'should appear relative to the name'))
260        add_option('imageonside', self.image_location)
261
262        self.image_size = EnumeratedListOption(_('Thumbnail size'), SIZE_NORMAL)
263        self.image_size.add_item(SIZE_NORMAL, _('Normal'))
264        self.image_size.add_item(SIZE_LARGE, _('Large'))
265        self.image_size.set_help(_('Size of the thumbnail image'))
266        add_option('imagesize', self.image_size)
267
268        # ----------------------------
269        add_option = partial(menu.add_option, _('Family Colors'))
270        # ----------------------------
271
272        surname_color = SurnameColorOption(_('Family colors'))
273        surname_color.set_help(_('Colors to use for various family lines.'))
274        add_option('surnamecolors', surname_color)
275
276        # -------------------------
277        add_option = partial(menu.add_option, _('Individuals'))
278        # -------------------------
279
280        color_males = ColorOption(_('Males'), '#e0e0ff')
281        color_males.set_help(_('The color to use to display men.'))
282        add_option('colormales', color_males)
283
284        color_females = ColorOption(_('Females'), '#ffe0e0')
285        color_females.set_help(_('The color to use to display women.'))
286        add_option('colorfemales', color_females)
287
288        color_unknown = ColorOption(_('Unknown'), '#e0e0e0')
289        color_unknown.set_help(_('The color to use '
290                                 'when the gender is unknown.'))
291        add_option('colorunknown', color_unknown)
292
293        color_family = ColorOption(_('Families'), '#ffffe0')
294        color_family.set_help(_('The color to use to display families.'))
295        add_option('colorfamilies', color_family)
296
297        self.limit_changed()
298        self.images_changed()
299
300    def limit_changed(self):
301        """
302        Handle the change of limiting parents and children.
303        """
304        self.max_parents.set_available(self.limit_parents.get_value())
305        self.max_children.set_available(self.limit_children.get_value())
306
307    def images_changed(self):
308        """
309        Handle the change of including images.
310        """
311        self.image_location.set_available(self.include_images.get_value())
312        self.image_size.set_available(self.include_images.get_value())
313
314    def include_dates_changed(self):
315        """
316        Enable/disable menu items if dates are required
317        """
318        if self.include_dates.get_value():
319            self.justyears.set_available(True)
320        else:
321            self.justyears.set_available(False)
322
323#------------------------------------------------------------------------
324#
325# FamilyLinesReport -- created once the user presses 'OK'
326#
327#------------------------------------------------------------------------
328class FamilyLinesReport(Report):
329    """ FamilyLines report """
330
331    def __init__(self, database, options, user):
332        """
333        Create FamilyLinesReport object that eventually produces the report.
334
335        The arguments are:
336
337        database     - the Gramps database instance
338        options      - instance of the FamilyLinesOptions class for this report
339        user         - a gen.user.User() instance
340        name_format  - Preferred format to display names
341        incl_private - Whether to include private data
342        inc_id       - Whether to include IDs.
343        living_people - How to handle living people
344        years_past_death - Consider as living this many years after death
345        """
346        Report.__init__(self, database, options, user)
347
348        menu = options.menu
349        get_option_by_name = menu.get_option_by_name
350        get_value = lambda name: get_option_by_name(name).get_value()
351
352        self.set_locale(menu.get_option_by_name('trans').get_value())
353
354        stdoptions.run_date_format_option(self, menu)
355
356        stdoptions.run_private_data_option(self, menu)
357        stdoptions.run_living_people_option(self, menu, self._locale)
358        self.database = CacheProxyDb(self.database)
359        self._db = self.database
360
361        # initialize several convenient variables
362        self._people = set() # handle of people we need in the report
363        self._families = set() # handle of families we need in the report
364        self._deleted_people = 0
365        self._deleted_families = 0
366        self._user = user
367
368        self._followpar = get_value('followpar')
369        self._followchild = get_value('followchild')
370        self._removeextra = get_value('removeextra')
371        self._gidlist = get_value('gidlist')
372        self._colormales = get_value('colormales')
373        self._colorfemales = get_value('colorfemales')
374        self._colorunknown = get_value('colorunknown')
375        self._colorfamilies = get_value('colorfamilies')
376        self._limitparents = get_value('limitparents')
377        self._maxparents = get_value('maxparents')
378        self._limitchildren = get_value('limitchildren')
379        self._maxchildren = get_value('maxchildren')
380        self._incimages = get_value('incimages')
381        self._imageonside = get_value('imageonside')
382        self._imagesize = get_value('imagesize')
383        self._useroundedcorners = get_value('useroundedcorners')
384        self._usesubgraphs = get_value('usesubgraphs')
385        self._incdates = get_value('incdates')
386        self._just_years = get_value('justyears')
387        self._incplaces = get_value('incplaces')
388        self._incchildcount = get_value('incchildcnt')
389        self.includeid = get_value('inc_id')
390
391        arrow_str = get_value('arrow')
392        if 'd' in arrow_str:
393            self._arrowheadstyle = 'normal'
394        else:
395            self._arrowheadstyle = 'none'
396        if 'a' in arrow_str:
397            self._arrowtailstyle = 'normal'
398        else:
399            self._arrowtailstyle = 'none'
400
401        # the gidlist is annoying for us to use since we always have to convert
402        # the GIDs to either Person or to handles, so we may as well convert the
403        # entire list right now and not have to deal with it ever again
404        self._interest_set = set()
405        if not self._gidlist:
406            raise ReportError(_('Empty report'),
407                              _('You did not specify anybody'))
408        for gid in self._gidlist.split():
409            person = self._db.get_person_from_gramps_id(gid)
410            if person:
411                #option can be from another family tree, so person can be None
412                self._interest_set.add(person.get_handle())
413
414        stdoptions.run_name_format_option(self, menu)
415
416        # convert the 'surnamecolors' string to a dictionary of names and colors
417        self._surnamecolors = {}
418        tmp = get_value('surnamecolors')
419        if tmp.find('\xb0') >= 0:
420            # new style delimiter (see bug report #2162)
421            tmp = tmp.split('\xb0')
422        else:
423            # old style delimiter
424            tmp = tmp.split(' ')
425
426        while len(tmp) > 1:
427            surname = tmp.pop(0).encode('iso-8859-1', 'xmlcharrefreplace')
428            colour = tmp.pop(0)
429            self._surnamecolors[surname] = colour
430
431        self._colorize = get_value('color')
432
433    def begin_report(self):
434        """
435        Inherited method; called by report() in _ReportDialog.py
436
437        This is where we'll do all of the work of figuring out who
438        from the database is going to be output into the report
439        """
440
441        # starting with the people of interest, we then add parents:
442        self._people.clear()
443        self._families.clear()
444        if self._followpar:
445            self.find_parents()
446
447            if self._removeextra:
448                self.remove_uninteresting_parents()
449
450        # ...and/or with the people of interest we add their children:
451        if self._followchild:
452            self.find_children()
453        # once we get here we have a full list of people
454        # and families that we need to generate a report
455
456
457    def write_report(self):
458        """
459        Inherited method; called by report() in _ReportDialog.py
460        """
461
462        # now that begin_report() has done the work, output what we've
463        # obtained into whatever file or format the user expects to use
464
465        self.doc.add_comment('# %s %d' %
466                             (self._('Number of people in database:'),
467                              self._db.get_number_of_people()))
468        self.doc.add_comment('# %s %d' %
469                             (self._('Number of people of interest:'),
470                              len(self._people)))
471        self.doc.add_comment('# %s %d' %
472                             (self._('Number of families in database:'),
473                              self._db.get_number_of_families()))
474        self.doc.add_comment('# %s %d' %
475                             (self._('Number of families of interest:'),
476                              len(self._families)))
477        if self._removeextra:
478            self.doc.add_comment('# %s %d' %
479                                 (self._('Additional people removed:'),
480                                  self._deleted_people))
481            self.doc.add_comment('# %s %d' %
482                                 (self._('Additional families removed:'),
483                                  self._deleted_families))
484        self.doc.add_comment('# %s' %
485                             self._('Initial list of people of interest:'))
486        for handle in self._interest_set:
487            person = self._db.get_person_from_handle(handle)
488            gid = person.get_gramps_id()
489            name = person.get_primary_name().get_regular_name()
490            # translators: needed for Arabic, ignore othewise
491            id_n = self._("%(str1)s, %(str2)s") % {'str1':gid, 'str2':name}
492            self.doc.add_comment('# -> ' + id_n)
493
494        self.write_people()
495        self.write_families()
496
497    def find_parents(self):
498        """ find the parents """
499        # we need to start with all of our "people of interest"
500        ancestors_not_yet_processed = set(self._interest_set)
501
502        # now we find all the immediate ancestors of our people of interest
503
504        while ancestors_not_yet_processed:
505            handle = ancestors_not_yet_processed.pop()
506
507            # One of 2 things can happen here:
508            #   1) we already know about this person and he/she is already
509            #      in our list
510            #   2) this is someone new, and we need to remember him/her
511            #
512            # In the first case, there isn't anything else to do, so we simply
513            # go back to the top and pop the next person off the list.
514            #
515            # In the second case, we need to add this person to our list, and
516            # then go through all of the parents this person has to find more
517            # people of interest.
518
519            if handle not in self._people:
520
521                person = self._db.get_person_from_handle(handle)
522
523                # remember this person!
524                self._people.add(handle)
525
526                # see if a family exists between this person and someone else
527                # we have on our list of people we're going to output -- if
528                # there is a family, then remember it for when it comes time
529                # to link spouses together
530                for family_handle in person.get_family_handle_list():
531                    family = self._db.get_family_from_handle(family_handle)
532                    if not family:
533                        continue
534                    spouse_handle = utils.find_spouse(person, family)
535                    if spouse_handle:
536                        if (spouse_handle in self._people or
537                                spouse_handle in ancestors_not_yet_processed):
538                            self._families.add(family_handle)
539
540                # if we have a limit on the number of people, and we've
541                # reached that limit, then don't attempt to find any
542                # more ancestors
543                if (self._limitparents and
544                        (self._maxparents <
545                         len(ancestors_not_yet_processed) + len(self._people))):
546                    # get back to the top of the while loop so we can finish
547                    # processing the people queued up in the "not yet
548                    # processed" list
549                    continue
550
551                # queue the parents of the person we're processing
552                for family_handle in person.get_parent_family_handle_list():
553                    family = self._db.get_family_from_handle(family_handle)
554
555                    father_handle = family.get_father_handle()
556                    if father_handle:
557                        father = self._db.get_person_from_handle(father_handle)
558                        if father:
559                            ancestors_not_yet_processed.add(father_handle)
560                            self._families.add(family_handle)
561
562                    mother_handle = family.get_mother_handle()
563                    if mother_handle:
564                        mother = self._db.get_person_from_handle(mother_handle)
565                        if mother:
566                            ancestors_not_yet_processed.add(mother_handle)
567                            self._families.add(family_handle)
568
569    def remove_uninteresting_parents(self):
570        """ remove any uninteresting parents """
571        # start with all the people we've already identified
572        unprocessed_parents = set(self._people)
573
574        while len(unprocessed_parents) > 0:
575            handle = unprocessed_parents.pop()
576            person = self._db.get_person_from_handle(handle)
577            if not person:
578                continue
579
580            # There are a few things we're going to need,
581            # so look it all up right now; such as:
582            # - who is the child?
583            # - how many children?
584            # - parents?
585            # - spouse?
586            # - is a person of interest?
587            # - spouse of a person of interest?
588            # - same surname as a person of interest?
589            # - spouse has the same surname as a person of interest?
590
591            child_handle = None
592            child_count = 0
593            spouse_handle = None
594            spouse_count = 0
595            father_handle = None
596            mother_handle = None
597            spouse_father_handle = None
598            spouse_mother_handle = None
599            spouse_surname = ""
600            surname = person.get_primary_name().get_surname()
601            surname = surname.encode('iso-8859-1', 'xmlcharrefreplace')
602
603            # first we get the person's father and mother
604            for family_handle in person.get_parent_family_handle_list():
605                family = self._db.get_family_from_handle(family_handle)
606                handle = family.get_father_handle()
607                if handle in self._people:
608                    father_handle = handle
609                handle = family.get_mother_handle()
610                if handle in self._people:
611                    mother_handle = handle
612
613            # now see how many spouses this person has
614            for family_handle in person.get_family_handle_list():
615                family = self._db.get_family_from_handle(family_handle)
616                handle = utils.find_spouse(person, family)
617                if handle in self._people:
618                    spouse_count += 1
619                    spouse = self._db.get_person_from_handle(handle)
620                    spouse_handle = handle
621                    spouse_surname = spouse.get_primary_name().get_surname()
622                    spouse_surname = spouse_surname.encode(
623                        'iso-8859-1', 'xmlcharrefreplace')
624
625                    # see if the spouse has parents
626                    if not spouse_father_handle and not spouse_mother_handle:
627                        for family_handle in \
628                                spouse.get_parent_family_handle_list():
629                            family = self._db.get_family_from_handle(
630                                family_handle)
631                            handle = family.get_father_handle()
632                            if handle in self._people:
633                                spouse_father_handle = handle
634                            handle = family.get_mother_handle()
635                            if handle in self._people:
636                                spouse_mother_handle = handle
637
638            # get the number of children that we think might be interesting
639            for family_handle in person.get_family_handle_list():
640                family = self._db.get_family_from_handle(family_handle)
641                for child_ref in family.get_child_ref_list():
642                    if child_ref.ref in self._people:
643                        child_count += 1
644                        child_handle = child_ref.ref
645
646            # we now have everything we need -- start looking for reasons
647            # why this is a person we need to keep in our list, and loop
648            # back to the top as soon as a reason is discovered
649
650            # if this person has many children of interest, then we
651            # automatically keep this person
652            if child_count > 1:
653                continue
654
655            # if this person has many spouses of interest, then we
656            # automatically keep this person
657            if spouse_count > 1:
658                continue
659
660            # if this person has parents, then we automatically keep
661            # this person
662            if father_handle is not None or mother_handle is not None:
663                continue
664
665            # if the spouse has parents, then we automatically keep
666            # this person
667            if (spouse_father_handle is not None or
668                    spouse_mother_handle is not None):
669                continue
670
671            # if this is a person of interest, then we automatically keep
672            if person.get_handle() in self._interest_set:
673                continue
674
675            # if the spouse is a person of interest, then we keep
676            if spouse_handle in self._interest_set:
677                continue
678
679            # if the surname (or the spouse's surname) matches a person
680            # of interest, then we automatically keep this person
681            keep_this_person = False
682            for person_of_interest_handle in self._interest_set:
683                person_of_interest = self._db.get_person_from_handle(
684                    person_of_interest_handle)
685                surname_of_interest = person_of_interest.get_primary_name()
686                surname_of_interest = surname_of_interest.get_surname().encode(
687                    'iso-8859-1', 'xmlcharrefreplace')
688                if (surname_of_interest == surname or
689                        surname_of_interest == spouse_surname):
690                    keep_this_person = True
691                    break
692
693            if keep_this_person:
694                continue
695
696            # if we have a special colour to use for this person,
697            # then we automatically keep this person
698            if surname in self._surnamecolors:
699                continue
700
701            # if we have a special colour to use for the spouse,
702            # then we automatically keep this person
703            if spouse_surname in self._surnamecolors:
704                continue
705
706            # took us a while,
707            # but if we get here then we can remove this person
708            self._deleted_people += 1
709            self._people.remove(person.get_handle())
710
711            # we can also remove any families to which this person belonged
712            for family_handle in person.get_family_handle_list():
713                if family_handle in self._families:
714                    self._deleted_families += 1
715                    self._families.remove(family_handle)
716
717            # if we have a spouse, then ensure we queue up the spouse
718            if spouse_handle:
719                if spouse_handle not in unprocessed_parents:
720                    unprocessed_parents.add(spouse_handle)
721
722            # if we have a child, then ensure we queue up the child
723            if child_handle:
724                if child_handle not in unprocessed_parents:
725                    unprocessed_parents.add(child_handle)
726
727
728    def find_children(self):
729        """ find any children """
730        # we need to start with all of our "people of interest"
731        children_not_yet_processed = set(self._interest_set)
732        children_to_include = set()
733
734        # now we find all the children of our people of interest
735
736        while len(children_not_yet_processed) > 0:
737            handle = children_not_yet_processed.pop()
738
739            if handle not in children_to_include:
740
741                person = self._db.get_person_from_handle(handle)
742
743                # remember this person!
744                children_to_include.add(handle)
745
746                # if we have a limit on the number of people, and we've
747                # reached that limit, then don't attempt to find any
748                # more children
749                if (self._limitchildren and
750                        (self._maxchildren <
751                         len(children_not_yet_processed) +
752                         len(children_to_include)
753                        )):
754                    # get back to the top of the while loop
755                    # so we can finish processing the people
756                    # queued up in the "not yet processed" list
757                    continue
758
759                # iterate through this person's families
760                for family_handle in person.get_family_handle_list():
761                    family = self._db.get_family_from_handle(family_handle)
762
763                    # queue up any children from this person's family
764                    for childref in family.get_child_ref_list():
765                        child = self._db.get_person_from_handle(childref.ref)
766                        children_not_yet_processed.add(child.get_handle())
767                        self._families.add(family_handle)
768
769                    # include the spouse from this person's family
770                    spouse_handle = utils.find_spouse(person, family)
771                    if spouse_handle:
772                        children_to_include.add(spouse_handle)
773                        self._families.add(family_handle)
774
775        # we now merge our temp set "children_to_include" into our master set
776        self._people.update(children_to_include)
777
778    def write_people(self):
779        """ write the people """
780
781        self.doc.add_comment('')
782
783        # If we're going to attempt to include images, then use the HTML style
784        # of .gv file.
785        use_html_output = False
786        if self._incimages:
787            use_html_output = True
788
789        # loop through all the people we need to output
790        for handle in sorted(self._people): # enable a diff
791            person = self._db.get_person_from_handle(handle)
792            name = self._name_display.display(person)
793            p_id = person.get_gramps_id()
794
795            # figure out what colour to use
796            gender = person.get_gender()
797            colour = self._colorunknown
798            if gender == Person.MALE:
799                colour = self._colormales
800            elif gender == Person.FEMALE:
801                colour = self._colorfemales
802
803            # see if we have surname colours that match this person
804            surname = person.get_primary_name().get_surname()
805            surname = surname.encode('iso-8859-1', 'xmlcharrefreplace')
806            if surname in self._surnamecolors:
807                colour = self._surnamecolors[surname]
808
809            # see if we have a birth/death or fallback dates we can use
810            if self._incdates or self._incplaces:
811                bth_event = get_birth_or_fallback(self._db, person)
812                dth_event = get_death_or_fallback(self._db, person)
813            else:
814                bth_event = None
815                dth_event = None
816
817            # output the birth or fallback event
818            birth_str = None
819            if bth_event and self._incdates:
820                date = bth_event.get_date_object()
821                if self._just_years and date.get_year_valid():
822                    birth_str = self.get_date( # localized year
823                        Date(date.get_year()))
824                else:
825                    birth_str = self.get_date(date)
826
827            # get birth place (one of:  hamlet, village, town, city, parish,
828            # county, province, region, state or country)
829            birthplace = None
830            if bth_event and self._incplaces:
831                birthplace = self.get_event_place(bth_event)
832
833            # see if we have a deceased date we can use
834            death_str = None
835            if dth_event and self._incdates:
836                date = dth_event.get_date_object()
837                if self._just_years and date.get_year_valid():
838                    death_str = self.get_date( # localized year
839                        Date(date.get_year()))
840                else:
841                    death_str = self.get_date(date)
842
843            # get death place (one of:  hamlet, village, town, city, parish,
844            # county, province, region, state or country)
845            deathplace = None
846            if dth_event and self._incplaces:
847                deathplace = self.get_event_place(dth_event)
848
849            # see if we have an image to use for this person
850            image_path = None
851            if self._incimages:
852                media_list = person.get_media_list()
853                if len(media_list) > 0:
854                    media_handle = media_list[0].get_reference_handle()
855                    media = self._db.get_media_from_handle(media_handle)
856                    media_mime_type = media.get_mime_type()
857                    if media_mime_type[0:5] == "image":
858                        image_path = get_thumbnail_path(
859                            media_path_full(self._db, media.get_path()),
860                            rectangle=media_list[0].get_rectangle(),
861                            size=self._imagesize)
862
863            # put the label together and output this person
864            label = ""
865            line_delimiter = '\\n'
866            if use_html_output:
867                line_delimiter = '<BR/>'
868
869            # if we have an image, then start an HTML table;
870            # remember to close the table afterwards!
871            if image_path:
872                label = ('<TABLE BORDER="0" CELLSPACING="2" CELLPADDING="0" '
873                         'CELLBORDER="0"><TR><TD><IMG SRC="%s"/></TD>' %
874                         image_path)
875                if self._imageonside == 0:
876                    label += '</TR><TR>'
877                label += '<TD>'
878
879            # at the very least, the label must have the person's name
880            label += html.escape(name)
881            if self.includeid == 1: # same line
882                label += " (%s)" % p_id
883            elif self.includeid == 2: # own line
884                label += "%s(%s)" % (line_delimiter, p_id)
885
886            if birth_str or death_str:
887                label += '%s(' % line_delimiter
888                if birth_str:
889                    label += '%s' % birth_str
890                label += ' – '
891                if death_str:
892                    label += '%s' % death_str
893                label += ')'
894            if birthplace or deathplace:
895                if birthplace == deathplace:
896                    deathplace = None    # no need to print the same name twice
897                label += '%s' % line_delimiter
898                if birthplace:
899                    label += '%s' % birthplace
900                if birthplace and deathplace:
901                    label += ' / '
902                if deathplace:
903                    label += '%s' % deathplace
904
905            # see if we have a table that needs to be terminated
906            if image_path:
907                label += '</TD></TR></TABLE>'
908            else:
909                # non html label is enclosed by "" so escape other "
910                label = label.replace('"', '\\\"')
911
912            shape = "box"
913            style = "solid"
914            border = colour
915            fill = colour
916
917            # do not use colour if this is B&W outline
918            if self._colorize == 'outline':
919                border = ""
920                fill = ""
921
922            if gender == person.FEMALE and ("f" in self._useroundedcorners):
923                style = "rounded"
924            elif gender == person.MALE and ("m" in self._useroundedcorners):
925                style = "rounded"
926            elif gender == person.UNKNOWN:
927                shape = "hexagon"
928
929            # if we're filling the entire node:
930            if self._colorize == 'filled':
931                style += ",filled"
932                border = ""
933
934            # we're done -- add the node
935            self.doc.add_node(p_id,
936                              label=label,
937                              shape=shape,
938                              color=border,
939                              style=style,
940                              fillcolor=fill,
941                              htmloutput=use_html_output)
942
943    def write_families(self):
944        """ write the families """
945
946        self.doc.add_comment('')
947        ngettext = self._locale.translation.ngettext # to see "nearby" comments
948
949        # loop through all the families we need to output
950        for family_handle in sorted(self._families): # enable a diff
951            family = self._db.get_family_from_handle(family_handle)
952            fgid = family.get_gramps_id()
953
954            # figure out a wedding date or placename we can use
955            wedding_date = None
956            wedding_place = None
957            if self._incdates or self._incplaces:
958                for event_ref in family.get_event_ref_list():
959                    event = self._db.get_event_from_handle(event_ref.ref)
960                    if (event.get_type() == EventType.MARRIAGE and
961                            (event_ref.get_role() == EventRoleType.FAMILY or
962                             event_ref.get_role() == EventRoleType.PRIMARY)):
963                        # get the wedding date
964                        if self._incdates:
965                            date = event.get_date_object()
966                            if self._just_years and date.get_year_valid():
967                                wedding_date = self.get_date( # localized year
968                                    Date(date.get_year()))
969                            else:
970                                wedding_date = self.get_date(date)
971                        # get the wedding location
972                        if self._incplaces:
973                            wedding_place = self.get_event_place(event)
974                        break
975
976            # figure out the number of children (if any)
977            children_str = None
978            if self._incchildcount:
979                child_count = len(family.get_child_ref_list())
980                if child_count >= 1:
981                    # translators: leave all/any {...} untranslated
982                    children_str = ngettext("{number_of} child",
983                                            "{number_of} children", child_count
984                                           ).format(number_of=child_count)
985
986            label = ''
987            fgid_already = False
988            if wedding_date:
989                if label != '':
990                    label += '\\n'
991                label += '%s' % wedding_date
992                if self.includeid == 1 and not fgid_already: # same line
993                    label += " (%s)" % fgid
994                    fgid_already = True
995            if wedding_place:
996                if label != '':
997                    label += '\\n'
998                label += '%s' % wedding_place
999                if self.includeid == 1 and not fgid_already: # same line
1000                    label += " (%s)" % fgid
1001                    fgid_already = True
1002            if self.includeid == 1 and not label:
1003                label = "(%s)" % fgid
1004                fgid_already = True
1005            elif self.includeid == 2 and not label: # own line
1006                label = "(%s)" % fgid
1007                fgid_already = True
1008            elif self.includeid == 2 and label and not fgid_already:
1009                label += "\\n(%s)" % fgid
1010                fgid_already = True
1011            if children_str:
1012                if label != '':
1013                    label += '\\n'
1014                label += '%s' % children_str
1015                if self.includeid == 1 and not fgid_already: # same line
1016                    label += " (%s)" % fgid
1017                    fgid_already = True
1018
1019            shape = "ellipse"
1020            style = "solid"
1021            border = self._colorfamilies
1022            fill = self._colorfamilies
1023
1024            # do not use colour if this is B&W outline
1025            if self._colorize == 'outline':
1026                border = ""
1027                fill = ""
1028
1029            # if we're filling the entire node:
1030            if self._colorize == 'filled':
1031                style += ",filled"
1032                border = ""
1033
1034            # we're done -- add the node
1035            self.doc.add_node(fgid, label, shape, border, style, fill)
1036
1037        # now that we have the families written,
1038        # go ahead and link the parents and children to the families
1039        for family_handle in self._families:
1040
1041            # get the parents for this family
1042            family = self._db.get_family_from_handle(family_handle)
1043            fgid = family.get_gramps_id()
1044            father_handle = family.get_father_handle()
1045            mother_handle = family.get_mother_handle()
1046
1047            self.doc.add_comment('')
1048
1049            if self._usesubgraphs and father_handle and mother_handle:
1050                self.doc.start_subgraph(fgid)
1051
1052            # see if we have a father to link to this family
1053            if father_handle:
1054                if father_handle in self._people:
1055                    father = self._db.get_person_from_handle(father_handle)
1056                    father_rn = father.get_primary_name().get_regular_name()
1057                    comment = self._("father: %s") % father_rn
1058                    self.doc.add_link(father.get_gramps_id(), fgid, "",
1059                                      self._arrowheadstyle, self._arrowtailstyle,
1060                                      comment=comment)
1061
1062            # see if we have a mother to link to this family
1063            if mother_handle:
1064                if mother_handle in self._people:
1065                    mother = self._db.get_person_from_handle(mother_handle)
1066                    mother_rn = mother.get_primary_name().get_regular_name()
1067                    comment = self._("mother: %s") % mother_rn
1068                    self.doc.add_link(mother.get_gramps_id(), fgid, "",
1069                                      self._arrowheadstyle, self._arrowtailstyle,
1070                                      comment=comment)
1071
1072            if self._usesubgraphs and father_handle and mother_handle:
1073                self.doc.end_subgraph()
1074
1075            # link the children to the family
1076            for childref in family.get_child_ref_list():
1077                if childref.ref in self._people:
1078                    child = self._db.get_person_from_handle(childref.ref)
1079                    child_rn = child.get_primary_name().get_regular_name()
1080                    comment = self._("child: %s") % child_rn
1081                    self.doc.add_link(fgid, child.get_gramps_id(), "",
1082                                      self._arrowheadstyle, self._arrowtailstyle,
1083                                      comment=comment)
1084
1085    def get_event_place(self, event):
1086        """ get the place of the event """
1087        place_text = ''
1088        place_handle = event.get_place_handle()
1089        if place_handle:
1090            place = self._db.get_place_from_handle(place_handle)
1091            if place:
1092                place_text = _pd.display(self._db, place)
1093                place_text = html.escape(place_text)
1094        return place_text
1095
1096    def get_date(self, date):
1097        """ return a formatted date """
1098        return html.escape(self._get_date(date))
1099