#
# Gramps - a GTK+/GNOME based genealogy program
#
# Copyright (C) 2007-2008  Stephane Charette
# Copyright (C) 2007-2008  Brian G. Matherly
# Copyright (C) 2009-2010  Gary Burton
# Contribution 2009 by     Bob Ham <rah@bash.sh>
# Copyright (C) 2010       Jakim Friant
# Copyright (C) 2011-2014  Paul Franklin
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
#

"""
Family Lines, a Graphviz-based plugin for Gramps.
"""

#------------------------------------------------------------------------
#
# python modules
#
#------------------------------------------------------------------------
from functools import partial
import html

#------------------------------------------------------------------------
#
# Set up logging
#
#------------------------------------------------------------------------
import logging
LOG = logging.getLogger(".FamilyLines")

#------------------------------------------------------------------------
#
# Gramps module
#
#------------------------------------------------------------------------
from gramps.gen.const import GRAMPS_LOCALE as glocale
_ = glocale.translation.gettext
from gramps.gen.lib import EventRoleType, EventType, Person, PlaceType, Date
from gramps.gen.utils.file import media_path_full
from gramps.gen.utils.thumbnails import (get_thumbnail_path, SIZE_NORMAL,
                                         SIZE_LARGE)
from gramps.gen.plug.report import Report
from gramps.gen.plug.report import utils
from gramps.gen.plug.report import MenuReportOptions
from gramps.gen.plug.report import stdoptions
from gramps.gen.plug.menu import (NumberOption, ColorOption, BooleanOption,
                                  EnumeratedListOption, PersonListOption,
                                  SurnameColorOption)
from gramps.gen.utils.db import get_birth_or_fallback, get_death_or_fallback
from gramps.gen.proxy import CacheProxyDb
from gramps.gen.errors import ReportError
from gramps.gen.display.place import displayer as _pd

#------------------------------------------------------------------------
#
# Constant options items
#
#------------------------------------------------------------------------
_COLORS = [{'name' : _("B&W outline"), 'value' : "outline"},
           {'name' : _("Colored outline"), 'value' : "colored"},
           {'name' : _("Color fill"), 'value' : "filled"}]

_ARROWS = [ { 'name' : _("Descendants <- Ancestors"),  'value' : 'd' },
            { 'name' : _("Descendants -> Ancestors"),  'value' : 'a' },
            { 'name' : _("Descendants <-> Ancestors"), 'value' : 'da' },
            { 'name' : _("Descendants - Ancestors"),   'value' : '' }]

_CORNERS = [ { 'name' : _("None"),  'value' : '' },
             { 'name' : _("Female"), 'value' : 'f' },
             { 'name' : _("Male"),   'value' : 'm' },
             { 'name' : _("Both"),  'value' : 'fm' }]

#------------------------------------------------------------------------
#
# A quick overview of the classes we'll be using:
#
#   class FamilyLinesOptions(MenuReportOptions)
#       - this class is created when the report dialog comes up
#       - all configuration controls for the report are created here
#
#   class FamilyLinesReport(Report)
#       - this class is created only after the user clicks on "OK"
#       - the actual report generation is done by this class
#
#------------------------------------------------------------------------

class FamilyLinesOptions(MenuReportOptions):
    """
    Defines all of the controls necessary
    to configure the FamilyLines report.
    """
    def __init__(self, name, dbase):
        self.limit_parents = None
        self.max_parents = None
        self.limit_children = None
        self.max_children = None
        self.include_images = None
        self.image_location = None
        self.justyears = None
        self.include_dates = None
        MenuReportOptions.__init__(self, name, dbase)

    def add_menu_options(self, menu):

        # ---------------------
        category_name = _('Report Options')
        add_option = partial(menu.add_option, category_name)
        # ---------------------

        followpar = BooleanOption(_('Follow parents to determine '
                                    '"family lines"'), True)
        followpar.set_help(_('Parents and their ancestors will be '
                             'considered when determining "family lines".'))
        add_option('followpar', followpar)

        followchild = BooleanOption(_('Follow children to determine '
                                      '"family lines"'), True)
        followchild.set_help(_('Children will be considered when '
                               'determining "family lines".'))
        add_option('followchild', followchild)

        remove_extra_people = BooleanOption(_('Try to remove extra '
                                              'people and families'), True)
        remove_extra_people.set_help(_('People and families not directly '
                                       'related to people of interest will '
                                       'be removed when determining '
                                       '"family lines".'))
        add_option('removeextra', remove_extra_people)

        arrow = EnumeratedListOption(_("Arrowhead direction"), 'd')
        for i in range( 0, len(_ARROWS) ):
            arrow.add_item(_ARROWS[i]["value"], _ARROWS[i]["name"])
        arrow.set_help(_("Choose the direction that the arrows point."))
        add_option("arrow", arrow)

        color = EnumeratedListOption(_("Graph coloring"), "filled")
        for i in range(len(_COLORS)):
            color.add_item(_COLORS[i]["value"], _COLORS[i]["name"])
        color.set_help(_("Males will be shown with blue, females "
                         "with red, unless otherwise set above for filled. "
                         "If the sex of an individual "
                         "is unknown it will be shown with gray."))
        add_option("color", color)

        roundedcorners = EnumeratedListOption(_("Rounded corners"), '')
        for i in range( 0, len(_CORNERS) ):
            roundedcorners.add_item(_CORNERS[i]["value"], _CORNERS[i]["name"])
        roundedcorners.set_help(_("Use rounded corners e.g. to differentiate "
                         "between women and men."))
        add_option("useroundedcorners", roundedcorners)

        stdoptions.add_gramps_id_option(menu, category_name, ownline=True)

        # ---------------------
        category_name = _('Report Options (2)')
        add_option = partial(menu.add_option, category_name)
        # ---------------------

        stdoptions.add_name_format_option(menu, category_name)

        stdoptions.add_private_data_option(menu, category_name, default=False)

        stdoptions.add_living_people_option(menu, category_name)

        locale_opt = stdoptions.add_localization_option(menu, category_name)

        stdoptions.add_date_format_option(menu, category_name, locale_opt)

        # --------------------------------
        add_option = partial(menu.add_option, _('People of Interest'))
        # --------------------------------

        person_list = PersonListOption(_('People of interest'))
        person_list.set_help(_('People of interest are used as a starting '
                               'point when determining "family lines".'))
        add_option('gidlist', person_list)

        self.limit_parents = BooleanOption(_('Limit the number of ancestors'),
                                           False)
        self.limit_parents.set_help(_('Whether to '
                                      'limit the number of ancestors.'))
        add_option('limitparents', self.limit_parents)
        self.limit_parents.connect('value-changed', self.limit_changed)

        self.max_parents = NumberOption('', 50, 10, 9999)
        self.max_parents.set_help(_('The maximum number '
                                    'of ancestors to include.'))
        add_option('maxparents', self.max_parents)

        self.limit_children = BooleanOption(_('Limit the number '
                                              'of descendants'),
                                            False)
        self.limit_children.set_help(_('Whether to '
                                       'limit the number of descendants.'))
        add_option('limitchildren', self.limit_children)
        self.limit_children.connect('value-changed', self.limit_changed)

        self.max_children = NumberOption('', 50, 10, 9999)
        self.max_children.set_help(_('The maximum number '
                                     'of descendants to include.'))
        add_option('maxchildren', self.max_children)

        # --------------------
        category_name = _('Include')
        add_option = partial(menu.add_option, category_name)
        # --------------------

        self.include_dates = BooleanOption(_('Include dates'), True)
        self.include_dates.set_help(_('Whether to include dates for people '
                                      'and families.'))
        add_option('incdates', self.include_dates)
        self.include_dates.connect('value-changed', self.include_dates_changed)

        self.justyears = BooleanOption(_("Limit dates to years only"), False)
        self.justyears.set_help(_("Prints just dates' year, neither "
                                  "month or day nor date approximation "
                                  "or interval are shown."))
        add_option("justyears", self.justyears)

        include_places = BooleanOption(_('Include places'), True)
        include_places.set_help(_('Whether to include placenames for people '
                                  'and families.'))
        add_option('incplaces', include_places)

        include_num_children = BooleanOption(_('Include the number of '
                                               'children'), True)
        include_num_children.set_help(_('Whether to include the number of '
                                        'children for families with more '
                                        'than 1 child.'))
        add_option('incchildcnt', include_num_children)

        self.include_images = BooleanOption(_('Include '
                                              'thumbnail images of people'),
                                            True)
        self.include_images.set_help(_('Whether to '
                                       'include thumbnail images of people.'))
        add_option('incimages', self.include_images)
        self.include_images.connect('value-changed', self.images_changed)

        self.image_location = EnumeratedListOption(_('Thumbnail location'), 0)
        self.image_location.add_item(0, _('Above the name'))
        self.image_location.add_item(1, _('Beside the name'))
        self.image_location.set_help(_('Where the thumbnail image '
                                       'should appear relative to the name'))
        add_option('imageonside', self.image_location)

        self.image_size = EnumeratedListOption(_('Thumbnail size'), SIZE_NORMAL)
        self.image_size.add_item(SIZE_NORMAL, _('Normal'))
        self.image_size.add_item(SIZE_LARGE, _('Large'))
        self.image_size.set_help(_('Size of the thumbnail image'))
        add_option('imagesize', self.image_size)

        # ----------------------------
        add_option = partial(menu.add_option, _('Family Colors'))
        # ----------------------------

        surname_color = SurnameColorOption(_('Family colors'))
        surname_color.set_help(_('Colors to use for various family lines.'))
        add_option('surnamecolors', surname_color)

        # -------------------------
        add_option = partial(menu.add_option, _('Individuals'))
        # -------------------------

        color_males = ColorOption(_('Males'), '#e0e0ff')
        color_males.set_help(_('The color to use to display men.'))
        add_option('colormales', color_males)

        color_females = ColorOption(_('Females'), '#ffe0e0')
        color_females.set_help(_('The color to use to display women.'))
        add_option('colorfemales', color_females)

        color_unknown = ColorOption(_('Unknown'), '#e0e0e0')
        color_unknown.set_help(_('The color to use '
                                 'when the gender is unknown.'))
        add_option('colorunknown', color_unknown)

        color_family = ColorOption(_('Families'), '#ffffe0')
        color_family.set_help(_('The color to use to display families.'))
        add_option('colorfamilies', color_family)

        self.limit_changed()
        self.images_changed()

    def limit_changed(self):
        """
        Handle the change of limiting parents and children.
        """
        self.max_parents.set_available(self.limit_parents.get_value())
        self.max_children.set_available(self.limit_children.get_value())

    def images_changed(self):
        """
        Handle the change of including images.
        """
        self.image_location.set_available(self.include_images.get_value())
        self.image_size.set_available(self.include_images.get_value())

    def include_dates_changed(self):
        """
        Enable/disable menu items if dates are required
        """
        if self.include_dates.get_value():
            self.justyears.set_available(True)
        else:
            self.justyears.set_available(False)

#------------------------------------------------------------------------
#
# FamilyLinesReport -- created once the user presses 'OK'
#
#------------------------------------------------------------------------
class FamilyLinesReport(Report):
    """ FamilyLines report """

    def __init__(self, database, options, user):
        """
        Create FamilyLinesReport object that eventually produces the report.

        The arguments are:

        database     - the Gramps database instance
        options      - instance of the FamilyLinesOptions class for this report
        user         - a gen.user.User() instance
        name_format  - Preferred format to display names
        incl_private - Whether to include private data
        inc_id       - Whether to include IDs.
        living_people - How to handle living people
        years_past_death - Consider as living this many years after death
        """
        Report.__init__(self, database, options, user)

        menu = options.menu
        get_option_by_name = menu.get_option_by_name
        get_value = lambda name: get_option_by_name(name).get_value()

        self.set_locale(menu.get_option_by_name('trans').get_value())

        stdoptions.run_date_format_option(self, menu)

        stdoptions.run_private_data_option(self, menu)
        stdoptions.run_living_people_option(self, menu, self._locale)
        self.database = CacheProxyDb(self.database)
        self._db = self.database

        # initialize several convenient variables
        self._people = set() # handle of people we need in the report
        self._families = set() # handle of families we need in the report
        self._deleted_people = 0
        self._deleted_families = 0
        self._user = user

        self._followpar = get_value('followpar')
        self._followchild = get_value('followchild')
        self._removeextra = get_value('removeextra')
        self._gidlist = get_value('gidlist')
        self._colormales = get_value('colormales')
        self._colorfemales = get_value('colorfemales')
        self._colorunknown = get_value('colorunknown')
        self._colorfamilies = get_value('colorfamilies')
        self._limitparents = get_value('limitparents')
        self._maxparents = get_value('maxparents')
        self._limitchildren = get_value('limitchildren')
        self._maxchildren = get_value('maxchildren')
        self._incimages = get_value('incimages')
        self._imageonside = get_value('imageonside')
        self._imagesize = get_value('imagesize')
        self._useroundedcorners = get_value('useroundedcorners')
        self._usesubgraphs = get_value('usesubgraphs')
        self._incdates = get_value('incdates')
        self._just_years = get_value('justyears')
        self._incplaces = get_value('incplaces')
        self._incchildcount = get_value('incchildcnt')
        self.includeid = get_value('inc_id')

        arrow_str = get_value('arrow')
        if 'd' in arrow_str:
            self._arrowheadstyle = 'normal'
        else:
            self._arrowheadstyle = 'none'
        if 'a' in arrow_str:
            self._arrowtailstyle = 'normal'
        else:
            self._arrowtailstyle = 'none'

        # the gidlist is annoying for us to use since we always have to convert
        # the GIDs to either Person or to handles, so we may as well convert the
        # entire list right now and not have to deal with it ever again
        self._interest_set = set()
        if not self._gidlist:
            raise ReportError(_('Empty report'),
                              _('You did not specify anybody'))
        for gid in self._gidlist.split():
            person = self._db.get_person_from_gramps_id(gid)
            if person:
                #option can be from another family tree, so person can be None
                self._interest_set.add(person.get_handle())

        stdoptions.run_name_format_option(self, menu)

        # convert the 'surnamecolors' string to a dictionary of names and colors
        self._surnamecolors = {}
        tmp = get_value('surnamecolors')
        if tmp.find('\xb0') >= 0:
            # new style delimiter (see bug report #2162)
            tmp = tmp.split('\xb0')
        else:
            # old style delimiter
            tmp = tmp.split(' ')

        while len(tmp) > 1:
            surname = tmp.pop(0).encode('iso-8859-1', 'xmlcharrefreplace')
            colour = tmp.pop(0)
            self._surnamecolors[surname] = colour

        self._colorize = get_value('color')

    def begin_report(self):
        """
        Inherited method; called by report() in _ReportDialog.py

        This is where we'll do all of the work of figuring out who
        from the database is going to be output into the report
        """

        # starting with the people of interest, we then add parents:
        self._people.clear()
        self._families.clear()
        if self._followpar:
            self.find_parents()

            if self._removeextra:
                self.remove_uninteresting_parents()

        # ...and/or with the people of interest we add their children:
        if self._followchild:
            self.find_children()
        # once we get here we have a full list of people
        # and families that we need to generate a report


    def write_report(self):
        """
        Inherited method; called by report() in _ReportDialog.py
        """

        # now that begin_report() has done the work, output what we've
        # obtained into whatever file or format the user expects to use

        self.doc.add_comment('# %s %d' %
                             (self._('Number of people in database:'),
                              self._db.get_number_of_people()))
        self.doc.add_comment('# %s %d' %
                             (self._('Number of people of interest:'),
                              len(self._people)))
        self.doc.add_comment('# %s %d' %
                             (self._('Number of families in database:'),
                              self._db.get_number_of_families()))
        self.doc.add_comment('# %s %d' %
                             (self._('Number of families of interest:'),
                              len(self._families)))
        if self._removeextra:
            self.doc.add_comment('# %s %d' %
                                 (self._('Additional people removed:'),
                                  self._deleted_people))
            self.doc.add_comment('# %s %d' %
                                 (self._('Additional families removed:'),
                                  self._deleted_families))
        self.doc.add_comment('# %s' %
                             self._('Initial list of people of interest:'))
        for handle in self._interest_set:
            person = self._db.get_person_from_handle(handle)
            gid = person.get_gramps_id()
            name = person.get_primary_name().get_regular_name()
            # translators: needed for Arabic, ignore othewise
            id_n = self._("%(str1)s, %(str2)s") % {'str1':gid, 'str2':name}
            self.doc.add_comment('# -> ' + id_n)

        self.write_people()
        self.write_families()

    def find_parents(self):
        """ find the parents """
        # we need to start with all of our "people of interest"
        ancestors_not_yet_processed = set(self._interest_set)

        # now we find all the immediate ancestors of our people of interest

        while ancestors_not_yet_processed:
            handle = ancestors_not_yet_processed.pop()

            # One of 2 things can happen here:
            #   1) we already know about this person and he/she is already
            #      in our list
            #   2) this is someone new, and we need to remember him/her
            #
            # In the first case, there isn't anything else to do, so we simply
            # go back to the top and pop the next person off the list.
            #
            # In the second case, we need to add this person to our list, and
            # then go through all of the parents this person has to find more
            # people of interest.

            if handle not in self._people:

                person = self._db.get_person_from_handle(handle)

                # remember this person!
                self._people.add(handle)

                # see if a family exists between this person and someone else
                # we have on our list of people we're going to output -- if
                # there is a family, then remember it for when it comes time
                # to link spouses together
                for family_handle in person.get_family_handle_list():
                    family = self._db.get_family_from_handle(family_handle)
                    if not family:
                        continue
                    spouse_handle = utils.find_spouse(person, family)
                    if spouse_handle:
                        if (spouse_handle in self._people or
                                spouse_handle in ancestors_not_yet_processed):
                            self._families.add(family_handle)

                # if we have a limit on the number of people, and we've
                # reached that limit, then don't attempt to find any
                # more ancestors
                if (self._limitparents and
                        (self._maxparents <
                         len(ancestors_not_yet_processed) + len(self._people))):
                    # get back to the top of the while loop so we can finish
                    # processing the people queued up in the "not yet
                    # processed" list
                    continue

                # queue the parents of the person we're processing
                for family_handle in person.get_parent_family_handle_list():
                    family = self._db.get_family_from_handle(family_handle)

                    father_handle = family.get_father_handle()
                    if father_handle:
                        father = self._db.get_person_from_handle(father_handle)
                        if father:
                            ancestors_not_yet_processed.add(father_handle)
                            self._families.add(family_handle)

                    mother_handle = family.get_mother_handle()
                    if mother_handle:
                        mother = self._db.get_person_from_handle(mother_handle)
                        if mother:
                            ancestors_not_yet_processed.add(mother_handle)
                            self._families.add(family_handle)

    def remove_uninteresting_parents(self):
        """ remove any uninteresting parents """
        # start with all the people we've already identified
        unprocessed_parents = set(self._people)

        while len(unprocessed_parents) > 0:
            handle = unprocessed_parents.pop()
            person = self._db.get_person_from_handle(handle)
            if not person:
                continue

            # There are a few things we're going to need,
            # so look it all up right now; such as:
            # - who is the child?
            # - how many children?
            # - parents?
            # - spouse?
            # - is a person of interest?
            # - spouse of a person of interest?
            # - same surname as a person of interest?
            # - spouse has the same surname as a person of interest?

            child_handle = None
            child_count = 0
            spouse_handle = None
            spouse_count = 0
            father_handle = None
            mother_handle = None
            spouse_father_handle = None
            spouse_mother_handle = None
            spouse_surname = ""
            surname = person.get_primary_name().get_surname()
            surname = surname.encode('iso-8859-1', 'xmlcharrefreplace')

            # first we get the person's father and mother
            for family_handle in person.get_parent_family_handle_list():
                family = self._db.get_family_from_handle(family_handle)
                handle = family.get_father_handle()
                if handle in self._people:
                    father_handle = handle
                handle = family.get_mother_handle()
                if handle in self._people:
                    mother_handle = handle

            # now see how many spouses this person has
            for family_handle in person.get_family_handle_list():
                family = self._db.get_family_from_handle(family_handle)
                handle = utils.find_spouse(person, family)
                if handle in self._people:
                    spouse_count += 1
                    spouse = self._db.get_person_from_handle(handle)
                    spouse_handle = handle
                    spouse_surname = spouse.get_primary_name().get_surname()
                    spouse_surname = spouse_surname.encode(
                        'iso-8859-1', 'xmlcharrefreplace')

                    # see if the spouse has parents
                    if not spouse_father_handle and not spouse_mother_handle:
                        for family_handle in \
                                spouse.get_parent_family_handle_list():
                            family = self._db.get_family_from_handle(
                                family_handle)
                            handle = family.get_father_handle()
                            if handle in self._people:
                                spouse_father_handle = handle
                            handle = family.get_mother_handle()
                            if handle in self._people:
                                spouse_mother_handle = handle

            # get the number of children that we think might be interesting
            for family_handle in person.get_family_handle_list():
                family = self._db.get_family_from_handle(family_handle)
                for child_ref in family.get_child_ref_list():
                    if child_ref.ref in self._people:
                        child_count += 1
                        child_handle = child_ref.ref

            # we now have everything we need -- start looking for reasons
            # why this is a person we need to keep in our list, and loop
            # back to the top as soon as a reason is discovered

            # if this person has many children of interest, then we
            # automatically keep this person
            if child_count > 1:
                continue

            # if this person has many spouses of interest, then we
            # automatically keep this person
            if spouse_count > 1:
                continue

            # if this person has parents, then we automatically keep
            # this person
            if father_handle is not None or mother_handle is not None:
                continue

            # if the spouse has parents, then we automatically keep
            # this person
            if (spouse_father_handle is not None or
                    spouse_mother_handle is not None):
                continue

            # if this is a person of interest, then we automatically keep
            if person.get_handle() in self._interest_set:
                continue

            # if the spouse is a person of interest, then we keep
            if spouse_handle in self._interest_set:
                continue

            # if the surname (or the spouse's surname) matches a person
            # of interest, then we automatically keep this person
            keep_this_person = False
            for person_of_interest_handle in self._interest_set:
                person_of_interest = self._db.get_person_from_handle(
                    person_of_interest_handle)
                surname_of_interest = person_of_interest.get_primary_name()
                surname_of_interest = surname_of_interest.get_surname().encode(
                    'iso-8859-1', 'xmlcharrefreplace')
                if (surname_of_interest == surname or
                        surname_of_interest == spouse_surname):
                    keep_this_person = True
                    break

            if keep_this_person:
                continue

            # if we have a special colour to use for this person,
            # then we automatically keep this person
            if surname in self._surnamecolors:
                continue

            # if we have a special colour to use for the spouse,
            # then we automatically keep this person
            if spouse_surname in self._surnamecolors:
                continue

            # took us a while,
            # but if we get here then we can remove this person
            self._deleted_people += 1
            self._people.remove(person.get_handle())

            # we can also remove any families to which this person belonged
            for family_handle in person.get_family_handle_list():
                if family_handle in self._families:
                    self._deleted_families += 1
                    self._families.remove(family_handle)

            # if we have a spouse, then ensure we queue up the spouse
            if spouse_handle:
                if spouse_handle not in unprocessed_parents:
                    unprocessed_parents.add(spouse_handle)

            # if we have a child, then ensure we queue up the child
            if child_handle:
                if child_handle not in unprocessed_parents:
                    unprocessed_parents.add(child_handle)


    def find_children(self):
        """ find any children """
        # we need to start with all of our "people of interest"
        children_not_yet_processed = set(self._interest_set)
        children_to_include = set()

        # now we find all the children of our people of interest

        while len(children_not_yet_processed) > 0:
            handle = children_not_yet_processed.pop()

            if handle not in children_to_include:

                person = self._db.get_person_from_handle(handle)

                # remember this person!
                children_to_include.add(handle)

                # if we have a limit on the number of people, and we've
                # reached that limit, then don't attempt to find any
                # more children
                if (self._limitchildren and
                        (self._maxchildren <
                         len(children_not_yet_processed) +
                         len(children_to_include)
                        )):
                    # get back to the top of the while loop
                    # so we can finish processing the people
                    # queued up in the "not yet processed" list
                    continue

                # iterate through this person's families
                for family_handle in person.get_family_handle_list():
                    family = self._db.get_family_from_handle(family_handle)

                    # queue up any children from this person's family
                    for childref in family.get_child_ref_list():
                        child = self._db.get_person_from_handle(childref.ref)
                        children_not_yet_processed.add(child.get_handle())
                        self._families.add(family_handle)

                    # include the spouse from this person's family
                    spouse_handle = utils.find_spouse(person, family)
                    if spouse_handle:
                        children_to_include.add(spouse_handle)
                        self._families.add(family_handle)

        # we now merge our temp set "children_to_include" into our master set
        self._people.update(children_to_include)

    def write_people(self):
        """ write the people """

        self.doc.add_comment('')

        # If we're going to attempt to include images, then use the HTML style
        # of .gv file.
        use_html_output = False
        if self._incimages:
            use_html_output = True

        # loop through all the people we need to output
        for handle in sorted(self._people): # enable a diff
            person = self._db.get_person_from_handle(handle)
            name = self._name_display.display(person)
            p_id = person.get_gramps_id()

            # figure out what colour to use
            gender = person.get_gender()
            colour = self._colorunknown
            if gender == Person.MALE:
                colour = self._colormales
            elif gender == Person.FEMALE:
                colour = self._colorfemales

            # see if we have surname colours that match this person
            surname = person.get_primary_name().get_surname()
            surname = surname.encode('iso-8859-1', 'xmlcharrefreplace')
            if surname in self._surnamecolors:
                colour = self._surnamecolors[surname]

            # see if we have a birth/death or fallback dates we can use
            if self._incdates or self._incplaces:
                bth_event = get_birth_or_fallback(self._db, person)
                dth_event = get_death_or_fallback(self._db, person)
            else:
                bth_event = None
                dth_event = None

            # output the birth or fallback event
            birth_str = None
            if bth_event and self._incdates:
                date = bth_event.get_date_object()
                if self._just_years and date.get_year_valid():
                    birth_str = self.get_date( # localized year
                        Date(date.get_year()))
                else:
                    birth_str = self.get_date(date)

            # get birth place (one of:  hamlet, village, town, city, parish,
            # county, province, region, state or country)
            birthplace = None
            if bth_event and self._incplaces:
                birthplace = self.get_event_place(bth_event)

            # see if we have a deceased date we can use
            death_str = None
            if dth_event and self._incdates:
                date = dth_event.get_date_object()
                if self._just_years and date.get_year_valid():
                    death_str = self.get_date( # localized year
                        Date(date.get_year()))
                else:
                    death_str = self.get_date(date)

            # get death place (one of:  hamlet, village, town, city, parish,
            # county, province, region, state or country)
            deathplace = None
            if dth_event and self._incplaces:
                deathplace = self.get_event_place(dth_event)

            # see if we have an image to use for this person
            image_path = None
            if self._incimages:
                media_list = person.get_media_list()
                if len(media_list) > 0:
                    media_handle = media_list[0].get_reference_handle()
                    media = self._db.get_media_from_handle(media_handle)
                    media_mime_type = media.get_mime_type()
                    if media_mime_type[0:5] == "image":
                        image_path = get_thumbnail_path(
                            media_path_full(self._db, media.get_path()),
                            rectangle=media_list[0].get_rectangle(),
                            size=self._imagesize)

            # put the label together and output this person
            label = ""
            line_delimiter = '\\n'
            if use_html_output:
                line_delimiter = '<BR/>'

            # if we have an image, then start an HTML table;
            # remember to close the table afterwards!
            if image_path:
                label = ('<TABLE BORDER="0" CELLSPACING="2" CELLPADDING="0" '
                         'CELLBORDER="0"><TR><TD><IMG SRC="%s"/></TD>' %
                         image_path)
                if self._imageonside == 0:
                    label += '</TR><TR>'
                label += '<TD>'

            # at the very least, the label must have the person's name
            label += html.escape(name)
            if self.includeid == 1: # same line
                label += " (%s)" % p_id
            elif self.includeid == 2: # own line
                label += "%s(%s)" % (line_delimiter, p_id)

            if birth_str or death_str:
                label += '%s(' % line_delimiter
                if birth_str:
                    label += '%s' % birth_str
                label += ' – '
                if death_str:
                    label += '%s' % death_str
                label += ')'
            if birthplace or deathplace:
                if birthplace == deathplace:
                    deathplace = None    # no need to print the same name twice
                label += '%s' % line_delimiter
                if birthplace:
                    label += '%s' % birthplace
                if birthplace and deathplace:
                    label += ' / '
                if deathplace:
                    label += '%s' % deathplace

            # see if we have a table that needs to be terminated
            if image_path:
                label += '</TD></TR></TABLE>'
            else:
                # non html label is enclosed by "" so escape other "
                label = label.replace('"', '\\\"')

            shape = "box"
            style = "solid"
            border = colour
            fill = colour

            # do not use colour if this is B&W outline
            if self._colorize == 'outline':
                border = ""
                fill = ""

            if gender == person.FEMALE and ("f" in self._useroundedcorners):
                style = "rounded"
            elif gender == person.MALE and ("m" in self._useroundedcorners):
                style = "rounded"
            elif gender == person.UNKNOWN:
                shape = "hexagon"

            # if we're filling the entire node:
            if self._colorize == 'filled':
                style += ",filled"
                border = ""

            # we're done -- add the node
            self.doc.add_node(p_id,
                              label=label,
                              shape=shape,
                              color=border,
                              style=style,
                              fillcolor=fill,
                              htmloutput=use_html_output)

    def write_families(self):
        """ write the families """

        self.doc.add_comment('')
        ngettext = self._locale.translation.ngettext # to see "nearby" comments

        # loop through all the families we need to output
        for family_handle in sorted(self._families): # enable a diff
            family = self._db.get_family_from_handle(family_handle)
            fgid = family.get_gramps_id()

            # figure out a wedding date or placename we can use
            wedding_date = None
            wedding_place = None
            if self._incdates or self._incplaces:
                for event_ref in family.get_event_ref_list():
                    event = self._db.get_event_from_handle(event_ref.ref)
                    if (event.get_type() == EventType.MARRIAGE and
                            (event_ref.get_role() == EventRoleType.FAMILY or
                             event_ref.get_role() == EventRoleType.PRIMARY)):
                        # get the wedding date
                        if self._incdates:
                            date = event.get_date_object()
                            if self._just_years and date.get_year_valid():
                                wedding_date = self.get_date( # localized year
                                    Date(date.get_year()))
                            else:
                                wedding_date = self.get_date(date)
                        # get the wedding location
                        if self._incplaces:
                            wedding_place = self.get_event_place(event)
                        break

            # figure out the number of children (if any)
            children_str = None
            if self._incchildcount:
                child_count = len(family.get_child_ref_list())
                if child_count >= 1:
                    # translators: leave all/any {...} untranslated
                    children_str = ngettext("{number_of} child",
                                            "{number_of} children", child_count
                                           ).format(number_of=child_count)

            label = ''
            fgid_already = False
            if wedding_date:
                if label != '':
                    label += '\\n'
                label += '%s' % wedding_date
                if self.includeid == 1 and not fgid_already: # same line
                    label += " (%s)" % fgid
                    fgid_already = True
            if wedding_place:
                if label != '':
                    label += '\\n'
                label += '%s' % wedding_place
                if self.includeid == 1 and not fgid_already: # same line
                    label += " (%s)" % fgid
                    fgid_already = True
            if self.includeid == 1 and not label:
                label = "(%s)" % fgid
                fgid_already = True
            elif self.includeid == 2 and not label: # own line
                label = "(%s)" % fgid
                fgid_already = True
            elif self.includeid == 2 and label and not fgid_already:
                label += "\\n(%s)" % fgid
                fgid_already = True
            if children_str:
                if label != '':
                    label += '\\n'
                label += '%s' % children_str
                if self.includeid == 1 and not fgid_already: # same line
                    label += " (%s)" % fgid
                    fgid_already = True

            shape = "ellipse"
            style = "solid"
            border = self._colorfamilies
            fill = self._colorfamilies

            # do not use colour if this is B&W outline
            if self._colorize == 'outline':
                border = ""
                fill = ""

            # if we're filling the entire node:
            if self._colorize == 'filled':
                style += ",filled"
                border = ""

            # we're done -- add the node
            self.doc.add_node(fgid, label, shape, border, style, fill)

        # now that we have the families written,
        # go ahead and link the parents and children to the families
        for family_handle in self._families:

            # get the parents for this family
            family = self._db.get_family_from_handle(family_handle)
            fgid = family.get_gramps_id()
            father_handle = family.get_father_handle()
            mother_handle = family.get_mother_handle()

            self.doc.add_comment('')

            if self._usesubgraphs and father_handle and mother_handle:
                self.doc.start_subgraph(fgid)

            # see if we have a father to link to this family
            if father_handle:
                if father_handle in self._people:
                    father = self._db.get_person_from_handle(father_handle)
                    father_rn = father.get_primary_name().get_regular_name()
                    comment = self._("father: %s") % father_rn
                    self.doc.add_link(father.get_gramps_id(), fgid, "",
                                      self._arrowheadstyle, self._arrowtailstyle,
                                      comment=comment)

            # see if we have a mother to link to this family
            if mother_handle:
                if mother_handle in self._people:
                    mother = self._db.get_person_from_handle(mother_handle)
                    mother_rn = mother.get_primary_name().get_regular_name()
                    comment = self._("mother: %s") % mother_rn
                    self.doc.add_link(mother.get_gramps_id(), fgid, "",
                                      self._arrowheadstyle, self._arrowtailstyle,
                                      comment=comment)

            if self._usesubgraphs and father_handle and mother_handle:
                self.doc.end_subgraph()

            # link the children to the family
            for childref in family.get_child_ref_list():
                if childref.ref in self._people:
                    child = self._db.get_person_from_handle(childref.ref)
                    child_rn = child.get_primary_name().get_regular_name()
                    comment = self._("child: %s") % child_rn
                    self.doc.add_link(fgid, child.get_gramps_id(), "",
                                      self._arrowheadstyle, self._arrowtailstyle,
                                      comment=comment)

    def get_event_place(self, event):
        """ get the place of the event """
        place_text = ''
        place_handle = event.get_place_handle()
        if place_handle:
            place = self._db.get_place_from_handle(place_handle)
            if place:
                place_text = _pd.display(self._db, place)
                place_text = html.escape(place_text)
        return place_text

    def get_date(self, date):
        """ return a formatted date """
        return html.escape(self._get_date(date))
