#
# Gramps - a GTK+/GNOME based genealogy program
#
# Copyright (C) 2000-2004  Donald N. Allingham
# Contributions by Lorenzo Cappelletti <lorenzo.cappelletti@email.it>
# Modified by Alex Roitman: convert to database interface, change coding style.
#
# 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., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
#

# $Id$

"Generate files/Relationship graph"

#------------------------------------------------------------------------
#
# python modules
#
#------------------------------------------------------------------------
import os
import string

from sets import Set
from time import asctime

#------------------------------------------------------------------------
#
# GNOME/gtk
#
#------------------------------------------------------------------------
import gtk

#------------------------------------------------------------------------
#
# GRAMPS modules
#
#------------------------------------------------------------------------
import Utils
import Report
import BaseDoc
import GenericFilter
import Errors

from gettext import gettext as _
from latin_utf8 import utf8_to_latin

#------------------------------------------------------------------------
#
# constants
#
#------------------------------------------------------------------------
# FIXME: These will be removed soon
#_scaled = 0
#_single = 1
#_multiple = 2
#
#_pagecount_map = {
#    _("Single (scaled)") : _scaled,
#    _("Single") : _single,
#    _("Multiple") : _multiple,
#    }

_PS_FONT = 'Helvetica'
_TT_FONT = 'FreeSans'

_FAM_NONE  = 0
_FAM_STACK = 1
_FAM_NODE  = 2

#------------------------------------------------------------------------
#
# RelGraphDialog
#
#------------------------------------------------------------------------
class RelGraphDialog(Report.ReportDialog):

    # Default graph options
    File            = None
    IndividualSet   = Set()
    ShowAsStack     = 0
    ShowFamilies    = 0
    IncludeDates    = 1
    JustYear        = 0
    PlaceCause      = 1
    IncludeUrl      = 1
    IncludeId       = 0
    Colorize        = 1
    FontStyle       = _TT_FONT
    ArrowHeadStyle  = 'none'
    ArrowTailStyle  = 'normal'
    AdoptionsDashed = 1
    Width           = 0
    Height          = 0
    HPages          = 1
    VPages          = 1
    TBMargin        = 0
    LRMargin        = 0

    report_options = {}

    def __init__(self,database,person):
        Report.ReportDialog.__init__(self,database,person,self.report_options)

    def get_title(self):
        """The window title for this dialog"""
        return "%s - %s - GRAMPS" % (_("Relationship Graph"),
                                     _("Graphical Reports"))

    def get_target_browser_title(self):
        """The title of the window created when the 'browse' button is
        clicked in the 'Save As' frame."""
        return _("Graphviz File")

    def get_print_pagecount_map(self):
        """Set up the list of possible page counts."""
        # FIXME: Remove soon.
        #return(_pagecount_map, _("Single (scaled)"))
        # No pagecount options, everything is ruled from the page tab.
        return (None,None) 

    def get_report_generations(self):
        """No generations option, no page breaks option."""
        return (0, 0)

    def get_report_filters(self):
        """Set up the list of possible content filters."""

        name = self.person.get_primary_name().get_name()

        all = GenericFilter.GenericFilter()
        all.set_name(_("Entire Database"))
        all.add_rule(GenericFilter.Everyone([]))

        des = GenericFilter.GenericFilter()
        des.set_name(_("Descendants of %s") % name)
        des.add_rule(GenericFilter.IsDescendantOf([self.person.get_handle()]))

        fam = GenericFilter.GenericFilter()
        fam.set_name(_("Descendant family members of %s") % name)
        fam.add_rule(GenericFilter.IsDescendantFamilyOf([self.person.get_handle()]))

        ans = GenericFilter.GenericFilter()
        ans.set_name(_("Ancestors of %s") % name)
        ans.add_rule(GenericFilter.IsAncestorOf([self.person.get_handle()]))

        com = GenericFilter.GenericFilter()
        com.set_name(_("People with common ancestor with %s") % name)
        com.add_rule(GenericFilter.HasCommonAncestorWith([self.person.get_handle()]))

        return [all, des, fam, ans, com]

    def add_user_options(self):
        self.arrowstyle_optionmenu = gtk.OptionMenu()
        menu = gtk.Menu()

        menuitem = gtk.MenuItem(_("Descendants <- Ancestors"))
        menuitem.set_data('t', ('none', 'normal'))
        menuitem.show()
        menu.append(menuitem)

        menuitem = gtk.MenuItem(_("Descendants -> Ancestors"))
        menuitem.set_data('t', ('normal', 'none'))
        menuitem.show()
        menu.append(menuitem)

        menuitem = gtk.MenuItem(_("Descendants <-> Ancestors"))
        menuitem.set_data('t', ('normal', 'normal'))
        menuitem.show()
        menu.append(menuitem)

        menuitem = gtk.MenuItem(_("Descendants - Ancestors"))
        menuitem.set_data('t', ('none', 'none'))
        menuitem.show()
        menu.append(menuitem)

        menu.set_active(0)

        self.arrowstyle_optionmenu.set_menu(menu)

        self.font_optionmenu = gtk.OptionMenu()
        menu = gtk.Menu()

        menuitem = gtk.MenuItem(_("TrueType"))
        menuitem.set_data('t', _TT_FONT)
        menuitem.show()
        menu.append(menuitem)

        menuitem = gtk.MenuItem(_("PostScript"))
        menuitem.set_data('t', _PS_FONT)
        menuitem.show()
        menu.append(menuitem)

        self.font_optionmenu.set_menu(menu)

        self.add_frame_option(_("GraphViz Options"),
                              _("Font Options"),
                              self.font_optionmenu,
                              _("Choose the font family."))

        self.add_frame_option(_("GraphViz Options"),
                              _("Arrowhead Options"),
                              self.arrowstyle_optionmenu,
                              _("Choose the direction that the arrows point."))


        self.fam_optionmenu = gtk.OptionMenu()
        menu = gtk.Menu()

        menuitem = gtk.MenuItem(_("Do not show families"))
        menuitem.set_data('f',_FAM_NONE)
        menuitem.show()
        menu.append(menuitem)

        menuitem = gtk.MenuItem(_("Show families as stacks"))
        menuitem.set_data('f',_FAM_STACK)
        menuitem.show()
        menu.append(menuitem)

        menuitem = gtk.MenuItem(_("Show families as nodes"))
        menuitem.set_data('f',_FAM_NODE)
        menuitem.show()
        menu.append(menuitem)

        self.fam_optionmenu.set_menu(menu)
        
        self.add_frame_option(_("GraphViz Options"),
                              _("Family Display Options"),
                              self.fam_optionmenu,
                              _("Choose how to display families."))

        msg = _("Include IDs")
        self.includeid_cb = gtk.CheckButton(msg)
        self.includeid_cb.set_active(self.IncludeId)
        self.add_frame_option(_("GraphViz Options"), '',
                              self.includeid_cb,
                              _("Include individual and family IDs."))

        msg = _("Include Birth, Marriage and Death Dates")
        self.includedates_cb = gtk.CheckButton(msg)
        self.includedates_cb.set_active(self.IncludeDates)
        self.add_frame_option(_("GraphViz Options"), '',
                              self.includedates_cb,
                              _("Include the dates that the individual "
                                "was born, got married and/or died "
                                "in the graph labels."))

        self.just_year_cb = gtk.CheckButton(_("Limit dates to years only"))
        self.just_year_cb.set_active(self.JustYear)
        self.add_frame_option(_("GraphViz Options"), '',
                              self.just_year_cb,
                              _("Prints just dates' year, neither "
                                "month or day nor date approximation "
                                "or interval are shown."))

        self.place_cause_cb = gtk.CheckButton(_("Place/cause when no date"))
        self.place_cause_cb.set_active(self.PlaceCause)
        self.includedates_cb.connect('toggled', self._grey_out_cb)
        self.add_frame_option(_("GraphViz Options"), '',
                              self.place_cause_cb,
                              _("When no birth, marriage, or death date "
                                "is available, the correspondent place field "
                                "(or cause field when blank) will be used."))

        self.includeurl_cb = gtk.CheckButton(_("Include URLs"))
        self.includeurl_cb.set_active(self.IncludeUrl)
        self.add_frame_option(_("GraphViz Options"), '',
                              self.includeurl_cb,
                              _("Include a URL in each graph node so "
                                "that PDF and imagemap files can be "
                                "generated that contain active links "
                                "to the files generated by the 'Generate "
                                "Web Site' report."))

        self.colorize_cb = gtk.CheckButton(_("Colorize Graph"))
        self.colorize_cb.set_active(self.Colorize)
        self.add_frame_option(_("GraphViz Options"),
                              '',
                              self.colorize_cb,
                              _("Males will be outlined in blue, females "
                                "will be outlined in pink.  If the sex of "
                                "an individual is unknown it will be "
                                "outlined in black."))

        self.adoptionsdashed_cb = gtk.CheckButton(_("Indicate non-birth relationships with dashed lines"))
        self.adoptionsdashed_cb.set_active(self.AdoptionsDashed)
        self.add_frame_option(_("GraphViz Options"),
                              '',
                              self.adoptionsdashed_cb,
                              _("Non-birth relationships will show up "
                                "as dashed lines in the graph."))

        tb_margin_adj = gtk.Adjustment(value=0.5, lower=0.25,
                                      upper=100.0, step_incr=0.25)
        lr_margin_adj = gtk.Adjustment(value=0.5, lower=0.25,
                                      upper=100.0, step_incr=0.25)

        self.tb_margin_sb = gtk.SpinButton(adjustment=tb_margin_adj, digits=2)
        self.lr_margin_sb = gtk.SpinButton(adjustment=lr_margin_adj, digits=2)

        self.add_frame_option(_("Page Options"),
                              _("Top & Bottom Margins"),
                              self.tb_margin_sb)
        self.add_frame_option(_("Page Options"),
                              _("Left & Right Margins"),
                              self.lr_margin_sb)

        hpages_adj = gtk.Adjustment(value=1, lower=1, upper=25, step_incr=1)
        vpages_adj = gtk.Adjustment(value=1, lower=1, upper=25, step_incr=1)

        self.hpages_sb = gtk.SpinButton(adjustment=hpages_adj, digits=0)
        self.vpages_sb = gtk.SpinButton(adjustment=vpages_adj, digits=0)

        self.add_frame_option(_("Page Options"),
                              _("Number of Horizontal Pages"),
                              self.hpages_sb,
                              _("GraphViz can create very large graphs by "
                                "spreading the graph across a rectangular "
                                "array of pages. This controls the number "
                                "pages in the array horizontally."))
        self.add_frame_option(_("Page Options"),
                              _("Number of Vertical Pages"),
                              self.vpages_sb,
                              _("GraphViz can create very large graphs "
                                "by spreading the graph across a "
                                "rectangular array of pages. This "
                                "controls the number pages in the array "
                                "vertically."))

    def _grey_out_cb (self, button):
        if button == self.includedates_cb:
            if button.get_active():
                self.just_year_cb.set_sensitive(1)
                self.place_cause_cb.set_sensitive(1)
            else:
                self.just_year_cb.set_sensitive(0)
                self.place_cause_cb.set_sensitive(0)

    def make_doc_menu(self):
        """Build a one item menu of document types that are
        appropriate for this report."""
        name = "Graphviz (dot)"
        menuitem = gtk.MenuItem (name)
        menuitem.set_data ("d", name)
        menuitem.set_data("paper",1)
        if os.system ("dot </dev/null 2>/dev/null") == 0:
            menuitem.set_data ("printable", _("Generate print output"))
        menuitem.show ()
        myMenu = gtk.Menu ()
        myMenu.append (menuitem)
        self.format_menu.set_menu(myMenu)

    def make_document(self):
        """Do Nothing.  This document will be created in the
        make_report routine."""
        pass

    def setup_style_frame(self):
        """The style frame is not used in this dialog."""
        pass

    def parse_style_frame(self):
        """The style frame is not used in this dialog."""
        pass

    def parse_other_frames(self):
        self.ShowAsStack = 0
        self.ShowFamilies = 0
        showfam = self.fam_optionmenu.get_menu().get_active().get_data('f')
        if showfam == _FAM_STACK:
            self.ShowAsStack = 1
        elif showfam == _FAM_NODE:
            self.ShowFamilies = 1
        self.IncludeDates = self.includedates_cb.get_active()
        self.JustYear = self.just_year_cb.get_active()
        self.PlaceCause = self.place_cause_cb.get_active()
        self.IncludeId = self.includeid_cb.get_active()
        self.IncludeUrl = self.includeurl_cb.get_active()
        self.Colorize = self.colorize_cb.get_active()
        self.FontStyle =\
            self.font_optionmenu.get_menu().get_active().get_data('t')
        self.ArrowHeadStyle, \
        self.ArrowTailStyle =\
            self.arrowstyle_optionmenu.get_menu().get_active().get_data('t')
        self.AdoptionsDashed = self.adoptionsdashed_cb.get_active()
        self.HPages = self.hpages_sb.get_value_as_int()
        self.VPages = self.vpages_sb.get_value_as_int()
        self.TBMargin = self.tb_margin_sb.get_value()
        self.LRMargin = self.lr_margin_sb.get_value()

    def make_report(self):
        """Create the object that will produce the GraphViz file."""
        self.Width = self.paper.get_width_inches()
        self.Height = self.paper.get_height_inches()

        self.File = open(self.target_path,"w")

        try:
            self.individual_set =\
                Set(self.filter.apply(self.db, self.db.get_person_keys()))
            self.individual_set.add(self.person.get_handle())
        except Errors.FilterError, msg:
            from QuestionDialog import ErrorDialog
            (m1,m2) = msg.messages()
            ErrorDialog(m1,m2)

        _writeDot(self)

        if self.print_report.get_active ():
            os.environ["DOT"] = self.target_path
            os.system ('dot -Tps "$DOT" | %s &' % Report.get_print_dialog_app ())

#------------------------------------------------------------------------
#
#
#
#------------------------------------------------------------------------
def report(database,person):
    RelGraphDialog(database,person)

#------------------------------------------------------------------------
#
# _writeDot
#
#------------------------------------------------------------------------
def _writeDot(self):
    """Write out to a file a relationship graph in dot format"""
    self.File.write("/* GRAMPS - Relationship graph\n")
    self.File.write(" *\n")
    self.File.write(" * Report options:\n")
    self.File.write(" *   font style         : %s\n" % self.FontStyle)
    self.File.write(" *   style arrow head   : %s\n" % self.ArrowHeadStyle)
    self.File.write(" *               tail   : %s\n" % self.ArrowTailStyle)
    self.File.write(" *   include URLs       : %s\n" % self.IncludeUrl)
    self.File.write(" *           IDs        : %s\n" % self.IncludeId)
    self.File.write(" *           dates      : %s\n" % self.IncludeDates)
    self.File.write(" *   just year          : %s\n" % self.JustYear)
    self.File.write(" *   place or cause     : %s\n" % self.PlaceCause)
    self.File.write(" *   colorize           : %s\n" % self.Colorize)
    self.File.write(" *   dashed adoptions   : %s\n" % self.AdoptionsDashed)
    self.File.write(" *   show families      : %s\n" % self.ShowFamilies)
    self.File.write(" *        as stack      : %s\n" % self.ShowAsStack)
    self.File.write(" *   margins top/bottm  : %s\n" % self.TBMargin)
    self.File.write(" *           left/right : %s\n" % self.LRMargin)
    self.File.write(" *   pages horizontal   : %s\n" % self.HPages)
    self.File.write(" *         vertical     : %s\n" % self.VPages)
    self.File.write(" *   page width         : %sin\n" % self.Width)
    self.File.write(" *        height        : %sin\n" % self.Height)
    self.File.write(" *\n")
    self.File.write(" * Generated on %s by GRAMPS\n" % asctime())
    self.File.write(" */\n\n")
    self.File.write("digraph GRAMPS_relationship_graph {\n")
    self.File.write("bgcolor=white;\n")
    self.File.write("rankdir=LR;\n")
    self.File.write("center=1;\n")
    self.File.write("margin=0.5;\n")
    self.File.write("ratio=fill;\n")
    self.File.write("size=\"%3.1f,%3.1f\";\n"
                    % ((self.Width*self.HPages) - (self.LRMargin*2) -
                       ((self.HPages - 1)*1.0),
                       (self.Height*self.VPages) - (self.TBMargin*2) -
                       ((self.VPages - 1)*1.0)))
    self.File.write("page=\"%3.1f,%3.1f\";\n" % (self.Width, self.Height))

    if len(self.individual_set) > 1:
        if self.ShowAsStack:
            _write_graph_record(self)
        else:
            _write_graph_box(self)

    self.File.write("}\n// File end")
    self.File.close()

#------------------------------------------------------------------------
#
# _write_graph_box
#
#------------------------------------------------------------------------
def _write_graph_box (self):
    """Write out a graph body where all individuals are separated boxes"""
    individual_nodes = Set()  # list of individual graph nodes
    family_nodes = Set()      # list of family graph nodes
    # Writes out a not for each individual
    self.File.write('\n// Individual nodes (box graph)\n')
    _write_node(self.File, shape='box', color='black', fontname=self.FontStyle)
    for individual_id in self.individual_set:
        individual_nodes.add(individual_id)
        (color, url) = _get_individual_data(self, individual_id)
        label = _get_individual_label(self, individual_id)
        _write_node(self.File, individual_id, label, color, url, 
                fontname=self.FontStyle)
    # Writes out a node for each family
    if self.ShowFamilies:
        self.File.write('\n// Family nodes (box graph)\n')
        _write_node(self.File, shape='ellipse', color='black',
                   fontname=self.FontStyle)
        for individual_id in individual_nodes:
            individual = self.db.try_to_find_person_from_handle(individual_id)
            for family_handle in individual.get_family_handle_list():
                if family_handle not in family_nodes:
                    family_nodes.add(family_handle)
                    label = _get_family_handle_label(self, family_handle)
                    _write_node(self.File, family_handle, label, 
                            fontname=self.FontStyle)
    # Links each individual to their parents/family
    self.File.write('\n// Individual edges\n')
    _write_edge(self.File, style="solid", 
                    arrowHead=self.ArrowHeadStyle,arrowTail=self.ArrowTailStyle)
    for individual_id in individual_nodes:
        individual = self.db.try_to_find_person_from_handle(individual_id)
        for family_handle, mother_rel_ship, father_rel_ship\
                in individual.get_parent_family_handle_list():
            family =  self.db.find_family_from_handle(family_handle)
            father_handle = family.get_father_handle()
            mother_handle = family.get_mother_handle()
            if self.ShowFamilies and family_handle in family_nodes:
                # edge from an individual to their family
                style = _get_edge_style(self, father_rel_ship, mother_rel_ship)
                _write_edge(self.File, individual_id, family_handle, style)
            else:
                # edge from an individual to their parents
                if father_handle and father_handle in individual_nodes:
                    _write_edge(self.File, individual_id, father_handle,
                               _get_edge_style(self, father_rel_ship))
                if mother_handle and mother_handle in individual_nodes:
                    _write_edge(self.File, individual_id, mother_handle,
                               _get_edge_style(self, mother_rel_ship))
    # Links each family to its components
    if self.ShowFamilies:
        self.File.write('\n// Family edges (box graph)\n')
        _write_edge(self.File, style="solid", 
                    arrowHead=self.ArrowHeadStyle,arrowTail=self.ArrowTailStyle)
        for family_handle in family_nodes:
            family = self.db.find_family_from_handle(family_handle)
            father_handle = family.get_father_handle()
            if father_handle and father_handle in individual_nodes:
                _write_edge(self.File, family_handle, father_handle)
            mother_handle = family.get_mother_handle()
            if mother_handle and mother_handle in individual_nodes:
                _write_edge(self.File, family_handle, mother_handle)
    # Statistics
    males = 0
    females = 0
    unknowns = 0
    for individual_id in individual_nodes:
        individual = self.db.try_to_find_person_from_handle(individual_id)
        if individual.get_gender() == individual.male:
            males = males + 1
        elif individual.get_gender() == individual.female:
            females = females + 1
        else:
            unknowns = unknowns + 1
    _write_stats(self.File, males, females, unknowns, len(family_nodes))

#------------------------------------------------------------------------
#
# _writeGraphRecord
#
#------------------------------------------------------------------------
def _write_graph_record (self):
    """Write out a graph body where families are rendered as records"""
    # Builds a dictionary of family records.
    # Each record is made of an individual married with zero or
    # more individuals.
    family_nodes = {}
    if isinstance(self.filter.get_rules()[0],
                  GenericFilter.IsDescendantFamilyOf):
        # With the IsDescendantFamilyOf filter, the IndividualSet
        # includes people which are not direct descendants of the
        # active person (practically, spouses of direct
        # discendants). Because we want the graph to highlight the
        # consanguinity, IndividualSet is split in two subsets:
        # naturalRelatives (direct descendants) and its complementary
        # subset (in-law relatives).
        filter = GenericFilter.GenericFilter()
        filter.add_rule(GenericFilter.IsDescendantOf([self.person.get_handle()]))
        natural_relatives =\
            Set(filter.apply(self.db, self.db.get_person_keys()))
        natural_relatives.add(self.person.get_handle())
    else:
        natural_relatives = self.individual_set
    self.File.write('\n// Family nodes (record graph)\n')
    _write_node(self.File, shape='record', color='black',
               fontname=self.FontStyle)
    for individual_id in natural_relatives:
        # If both husband and wife are members of the IndividualSet,
        # only one record, with the husband first, is displayed.
        individual = self.db.try_to_find_person_from_handle(individual_id)
        if individual.get_gender() == individual.female:
            # There are exactly three cases where a female node is added:
            family_handle = None        # no family
            husbands = []           # filtered-in husbands (naturalRelatives)
            unknown_husbands = 0    # filtered-out/unknown husbands
            for family_handle in individual.get_family_handle_list():
                family = self.db.find_family_from_handle(family_handle)
                husband_id = family.get_father_handle()
                if husband_id and husband_id in self.individual_set:
                    if husband_id not in natural_relatives:
                        husbands.append(husband_id)
                else:
                    unknown_husbands = 1
            if not family_handle or len(husbands) or unknown_husbands:
                family_nodes[individual_id] = [individual_id] + husbands
        else:
            family_nodes[individual_id] = [individual_id]
            for family_handle in individual.get_family_handle_list():
                family = self.db.find_family_from_handle(family_handle)
                wife_id = family.get_mother_handle()
                if wife_id in self.individual_set:
                    family_nodes[individual_id].append(wife_id)
    # Writes out all family records
    for individual_id, family_handle in family_nodes.items():
        (color, url) = _get_individual_data(self, family_nodes[individual_id][0])
        label = _get_family_handle_record_label(self, family_nodes[individual_id])
        _write_node(self.File, individual_id, label, color, url, 
                fontname=self.FontStyle)
    # Links individual's record to their parents' record
    # The arrow goes from the individual port of a family record
    # to the parent port of the parent's family record.
    self.File.write('\n// Individual edges\n')
    _write_edge(self.File, style="solid", 
                arrowHead=self.ArrowHeadStyle,arrowTail=self.ArrowTailStyle)
    for family_from_handle, family_from_handle2 in family_nodes.items():
        for individual_from_handle in family_from_handle2:
            individual_from = self.db.try_to_find_person_from_handle(individual_from_handle)
            for family_handle, mother_rel_ship, father_rel_ship\
                    in individual_from.get_parent_family_handle_list():
                family = self.db.find_family_from_handle(family_handle)
                father_handle = family.get_father_handle()
                mother_handle = family.get_mother_handle()
                # Things are complicated here because a parent may or
                # or may not exist.
                if not father_handle:
                    father_handle = ""
                if not mother_handle:
                    mother_handle = ""
                if family_nodes.has_key(father_handle):
                    if mother_handle in family_nodes[father_handle]:
                        _write_edge(self.File, family_from_handle, father_handle,
                                   _get_edge_style(self, mother_rel_ship),
                                   portFrom=individual_from_handle, portTo=mother_handle)
                    else:
                        _write_edge(self.File, family_from_handle, father_handle,
                                   _get_edge_style(self, mother_rel_ship),
                                   portFrom=individual_from_handle)
                if family_nodes.has_key(mother_handle):
                    if father_handle in family_nodes[mother_handle]:
                        _write_edge(self.File, family_from_handle, mother_handle,
                                   _get_edge_style(self, father_rel_ship),
                                   portFrom=individual_from_handle, portTo=father_handle)
                    else:
                        _write_edge(self.File, family_from_handle, mother_handle,
                                   _get_edge_style(self, mother_rel_ship),
                                   portFrom=individual_from_handle)
    # Stats (unique individuals)
    males = Set()
    females = Set()
    unknowns = Set()
    marriages = 0
    for family_handle, family_handle2 in family_nodes.items():
        marriages = marriages + (len(family_handle2) - 1)
        for individual_id in family_handle2:
            individual = self.db.try_to_find_person_from_handle(individual_id)
            if individual.get_gender() == individual.male\
                   and individual_id not in males:
                males.add(individual_id)
            elif individual.get_gender() == individual.female\
                     and individual_id not in females:
                females.add(individual_id)
            elif individual.get_gender() == individual.unknown\
                     and individual_id not in unknowns:
                unknowns.add(individual_id)
    _write_stats(self.File, len(males), len(females), len(unknowns), marriages)

#------------------------------------------------------------------------
#
# _get_individual_data
#
#------------------------------------------------------------------------
def _get_individual_data (self, individual_id):
    """Returns a tuple of individual data"""
    # color
    color = ''
    individual = self.db.try_to_find_person_from_handle(individual_id)
    if self.Colorize:
        gender = individual.get_gender()
        if gender == individual.male:
            color = 'dodgerblue4'
        elif gender == individual.female:
            color  = 'deeppink'
    # url
    url = ''
    if self.IncludeUrl:
        url = "%s.html" % individual_id

    return (color, url)

#------------------------------------------------------------------------
#
# _get_event_label
#
#------------------------------------------------------------------------
def _get_event_label (self, event_handle):
    """Returns a formatted string of event data suitable for a label"""
    if self.IncludeDates and event_handle:
        event = self.db.find_event_from_handle(event_handle)
        date_obj = event.get_date_object()
        if date_obj.get_year_valid():
            if self.JustYear:
                return "%i" % date_obj.get_year()
            else:
                return date_obj.get_date()
        elif self.PlaceCause:
            place_handle = event.get_place_handle()
            if place_handle:
                place = self.db.try_to_find_place_from_handle(place_handle)
                return place.get_title()
            else:
                return event.get_cause()
    return ''

#------------------------------------------------------------------------
#
# _get_individual_label
#
#------------------------------------------------------------------------
def _get_individual_label (self, individual_id, marriage_event_handle=None, family_handle=None):
    """Returns a formatted string of individual data suitable for a label

    Returned string always includes individual's name and optionally
    individual's birth and death dates, individual's marriage date,
    individual's and family's IDs.
    """
    # Get data ready
    individual = self.db.try_to_find_person_from_handle(individual_id)
    name = individual.get_primary_name().get_name()
    if self.IncludeDates:
        birth = _get_event_label(self, individual.get_birth_handle())
        death = _get_event_label(self, individual.get_death_handle())
        if marriage_event_handle:
            marriage = _get_event_label(self, marriage_event_handle)
    # Id
    if self.IncludeId:
        if marriage_event_handle:
            label = "%s (%s)\\n" % (family_handle, individual_id)
        else:
            label = "%s\\n" % individual_id
    else:
        label = ""
    # Marriage date
    if self.IncludeDates and (marriage_event_handle and marriage):
        label = label + "%s\\n" % marriage
    # Individual's name
    label = label + name
    # Birth and death
    if self.IncludeDates and (birth or death):
        label = label + "\\n%s - %s" % (birth, death)
    return label

#------------------------------------------------------------------------
#
# _get_edge_style
#
#------------------------------------------------------------------------
def _get_edge_style (self, father_rel_ship, mother_rel_ship="Birth"):
    """Returns a edge style that depends on the relationships with parents"""
    if self.AdoptionsDashed and \
           (father_rel_ship != "Birth" or mother_rel_ship != "Birth"):
        return "dashed"


#------------------------------------------------------------------------
#
# _get_family_handle_label
#
#------------------------------------------------------------------------
def _get_family_handle_label (self, family_handle):
    """Returns a formatted string of family data suitable for a label"""

    fam = self.db.find_family_from_handle(family_handle)
    for event_handle in fam.get_event_list():
        if event_handle:
            event = self.db.find_event_from_handle(event_handle)
            if event.get_name() == "Marriage":
                marriage_event_handle = event_handle
                break
    else:
        marriage_event_handle = None

    marriage = _get_event_label(self, marriage_event_handle)
    if self.IncludeId:
        return "%s\\n%s" % (family_handle, marriage)
    else:
        return marriage

#------------------------------------------------------------------------
#
# _get_family_handle_record_label
#
#------------------------------------------------------------------------
def _get_family_handle_record_label (self, record):
    """Returns a formatted string of a family record suitable for a label"""
    labels = []
    spouse_id = record[0]
    spouse = self.db.try_to_find_person_from_handle(spouse_id)
    for individual_id in record:
        individual = self.db.try_to_find_person_from_handle(individual_id)
        if spouse_id == individual_id:
            label = _get_individual_label(self, individual_id)
        else:
            marriage_event_handle = None
            for individual_family_handle in individual.get_family_handle_list():
                if individual_family_handle in spouse.get_family_handle_list():
                    individual_family = self.db.find_family_from_handle(individual_family_handle)
                    for event_handle in individual_family.get_event_list():
                        if event_handle:
                            event = self.db.find_event_from_handle(event_handle)
                            if event.get_name() == "Marriage":
                                marriage_event_handle = event_handle
                                break

            label = _get_individual_label(self, individual_id, 
                        marriage_event_handle,individual_family_handle)
        label = string.replace(label, "|", r"\|")
        label = string.replace(label, "<", r"\<")
        label = string.replace(label, ">", r"\>")
        labels.append("<%s>%s" % (individual_id, label))
    return string.join(labels, "|")

#------------------------------------------------------------------------
#
# _write_node
#
#------------------------------------------------------------------------
def _write_node (file, node="node", label="", color="", url="", shape="",
               fontname=""):
    """Writes out an individual node"""
    file.write('%s [' % node)
    if label:
        if fontname == _TT_FONT:
            file.write('label="%s" ' % label.replace('"', r'\"'))
        else:
            file.write('label="%s" ' %
                   utf8_to_latin(label.replace('"', r'\"')))

    if color:
        file.write('color=%s ' % color)
    if url:
        file.write('URL="%s" ' % string.replace(url, '"', r'\"'))
    if shape:
        file.write('shape=%s ' % shape)
    if fontname:
        file.write('fontname="%s" ' % fontname)
    file.write('];\n')

#------------------------------------------------------------------------
#
# _write_edge
#
#------------------------------------------------------------------------
def _write_edge (file, nodeFrom="", nodeTo="", style="",
               arrowHead="", arrowTail="", portFrom="", portTo=""):
    """Writes out an edge"""
    if nodeFrom and nodeTo:
        if portFrom:
            frm = nodeFrom + ":" + portFrom
        else:
            frm = nodeFrom
        if portTo:
            to = nodeTo + ":" + portTo
        else:
            to = nodeTo
        file.write('%s -> %s [' % (frm, to))
    else:
        file.write('edge [')  # default edge
    if style:
        file.write('style=%s ' % style)
    if arrowHead:
        file.write('arrowhead=%s ' % arrowHead)
    if arrowTail:
        file.write('arrowtail=%s ' % arrowTail)
    file.write('];\n')

#------------------------------------------------------------------------
#
# _writeStats
#
#------------------------------------------------------------------------
def _write_stats (file, males, females, unknowns, marriages):
    file.write('\n/* Statistics\n')
    file.write(' *   individuals male    : % 4d\n' % males)
    file.write(' *               female  : % 4d\n' % females)
    file.write(' *               unknown : % 4d\n' % unknowns)
    file.write(' *               total   : % 4d\n' % (males+females+unknowns))
    file.write(' *   marriages           : % 4d\n' % marriages)
    file.write(' */\n')

#------------------------------------------------------------------------
#
#
#
#------------------------------------------------------------------------
def get_description():
    return _("Generates relationship graphs, currently only in GraphViz "
             "format. GraphViz (dot) can transform the graph into "
             "postscript, jpeg, png, vrml, svg, and many other formats. "
             "For more information or to get a copy of GraphViz, "
             "goto http://www.graphviz.org")

#------------------------------------------------------------------------
#
#
#
#------------------------------------------------------------------------
from Plugins import register_report

register_report(
    report,
    _("Relationship Graph"),
    status=(_("Beta")),
    category=_("Graphical Reports"),
    description=get_description(),
    author_name="Donald N. Allingham",
    author_email="dallingham@users.sourceforge.net"
    )
