work in progress converting FamilyLines to new GraphViz plugin

svn: r9612
This commit is contained in:
Stéphane Charette 2007-12-28 10:08:19 +00:00
parent 8eab7c6af2
commit 5ea0cd4b3e
8 changed files with 1085 additions and 31 deletions

View File

@ -1,3 +1,15 @@
2007-12-28 Stéphane Charette <stephanecharette@gmail.com>
* src/ReportBase/_GraphvizReportDialog.py
* src/plugins/GVFamilyLines.py
* src/plugins/GVRelGraph.py
* src/plugins/Makefile.am
* src/PluginUtils/__init__.py
* src/PluginUtils/_MenuOptions.py
* po/POTFILES.in
Work in progress; partial conversion of FamilyLines to the new
GraphViz report class. (Doesn't yet work, keep using the old one
in "Code Generators" until the bugs are fully ironed out.)
2007-12-27 Brian Matherly <brian@gramps-project.org>
* src/plugins/DetDescendantReport.py:
* src/plugins/DetAncestralReport.py:

View File

@ -275,6 +275,7 @@ src/plugins/FamilyLines.py
src/plugins/FanChart.py
src/plugins/FindDupes.py
src/plugins/GraphViz.py
src/plugins/GVFamilyLines.py
src/plugins/GVHourGlass.py
src/plugins/GVRelGraph.py
src/plugins/ImportCSV.py

View File

@ -27,7 +27,10 @@ Abstracted option handling.
# gramps modules
#
#-------------------------------------------------------------------------
import gobject
import _Tool as Tool
import GrampsWidgets
from Selectors import selector_factory
#-------------------------------------------------------------------------
#
@ -159,7 +162,46 @@ class StringOption(Option):
Parse the string option (single line text).
"""
return self.gobj.get_text()
#-------------------------------------------------------------------------
#
# ColourButtonOption class
#
#-------------------------------------------------------------------------
class ColourButtonOption(Option):
"""
This class describes an option that allows the selection of a colour.
"""
def __init__(self,label,value):
"""
@param label: A friendly label to be applied to this option.
Example: "Males"
@type label: string
@param value: An initial value for this option.
Example: "#ff00a0"
@type value: string, interpreted as a colour by gtk.gdk.color_parse()
@return: nothing
"""
Option.__init__(self,label,value)
def make_gui_obj(self, gtk, dialog):
"""
Add a ColorButton to the dialog.
"""
value = self.get_value()
self.gobj = gtk.ColorButton(gtk.gdk.color_parse(value))
def parse(self):
"""
Parse the colour and return as a string.
"""
colour = self.gobj.get_color()
value = '#%02x%02x%02x' % (
int(colour.red * 256 / 65536),
int(colour.green * 256 / 65536),
int(colour.blue * 256 / 65536))
return value
#-------------------------------------------------------------------------
#
# NumberOption class
@ -543,6 +585,137 @@ class FilterListOption(Option):
self.__value = int(self.combo.get_active())
return self.__value
#-------------------------------------------------------------------------
#
# PeoplePickerOption class
#
#-------------------------------------------------------------------------
class PeoplePickerOption(Option):
"""
This class describes a widget that allows
people from the database to be selected.
"""
def __init__(self, label, value, db):
"""
@param label: A friendly label to be applied to this option.
Example: "People of interest"
@type label: string
@param value: A set of GIDs as initial values for this option.
Example: "111 222 333 444"
@type value: set()
@return: nothing
"""
self.db = db
Option.__init__(self,label,value)
def make_gui_obj(self, gtk, dialog):
"""
Add a "people picker" widget to the dialog.
"""
value = self.get_value()
self.model = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING)
self.treeView = gtk.TreeView(self.model)
self.treeView.set_size_request(150, 150)
col1 = gtk.TreeViewColumn(_('Name' ), gtk.CellRendererText(), text=0)
col2 = gtk.TreeViewColumn(_('ID' ), gtk.CellRendererText(), text=1)
col1.set_resizable(True)
col2.set_resizable(True)
col1.set_sizing(gtk.TREE_VIEW_COLUMN_AUTOSIZE)
col2.set_sizing(gtk.TREE_VIEW_COLUMN_AUTOSIZE)
col1.set_sort_column_id(0)
col2.set_sort_column_id(1)
self.treeView.append_column(col1)
self.treeView.append_column(col2)
self.scrolledWindow = gtk.ScrolledWindow()
self.scrolledWindow.add(self.treeView)
self.scrolledWindow.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
self.scrolledWindow.set_shadow_type(gtk.SHADOW_OUT)
self.hbox = gtk.HBox()
self.hbox.pack_start(self.scrolledWindow, expand=True, fill=True)
if not self.db:
print "PROBLEM: from where can I obtain or pass in a db parm?"
else:
for gid in value.split():
person = self.db.get_person_from_gramps_id(gid)
if person:
name = _nd.display(person)
self.model.append([name, gid])
# now setup the '+' and '-' pushbutton for adding/removing people from the container
self.addPerson = GrampsWidgets.SimpleButton(gtk.STOCK_ADD, self.addPersonClicked)
self.delPerson = GrampsWidgets.SimpleButton(gtk.STOCK_REMOVE, self.delPersonClicked)
self.vbbox = gtk.VButtonBox()
self.vbbox.add(self.addPerson)
self.vbbox.add(self.delPerson)
self.vbbox.set_layout(gtk.BUTTONBOX_SPREAD)
self.hbox.pack_end(self.vbbox, expand=False)
# parent expects the widget as "self.gobj"
self.gobj = self.hbox
def parse(self):
"""
Parse the object and return.
"""
gidlist = ''
iter = self.model.get_iter_first()
while (iter):
gid = self.model.get_value(iter, 1)
gidlist = gidlist + gid + ' '
iter = self.model.iter_next(iter)
return gidlist
def addPersonClicked(self, obj):
# people we already have must be excluded
# so we don't list them multiple times
if not self.db:
print "PROBLEM: this method needs a db parm, and various other things like db, uistate, and track!"
skipList = set()
iter = self.model.get_iter_first()
while (iter):
gid = self.model.get_value(iter, 1) # get the GID stored in column #1
person = self.db.get_person_from_gramps_id(gid)
skipList.add(person.get_handle())
iter = self.model.iter_next(iter)
SelectPerson = selector_factory('Person')
sel = SelectPerson(self.dbstate, self.uistate, self.track, skip=skipList)
person = sel.run()
if person:
name = _nd.display(person)
gid = person.get_gramps_id()
self.model.append([name, gid])
# if this person has a spouse, ask if we should include the spouse
# in the list of "people of interest"
familyList = person.get_family_handle_list()
if familyList:
for familyHandle in familyList:
family = self.db.get_family_from_handle(familyHandle)
spouseHandle = ReportUtils.find_spouse(person, family)
if spouseHandle:
if spouseHandle not in skipList:
spouse = self.db.get_person_from_handle(spouseHandle)
text = _('Also include %s?') % spouse.get_primary_name().get_regular_name()
prompt = gtk.MessageDialog(parent=self.window, flags=gtk.DIALOG_MODAL, type=gtk.MESSAGE_QUESTION, buttons=gtk.BUTTONS_YES_NO, message_format=text)
prompt.set_default_response(gtk.RESPONSE_YES)
prompt.set_position(gtk.WIN_POS_CENTER_ON_PARENT)
prompt.set_title(_('Select Person'))
button = prompt.run()
prompt.destroy()
if button == gtk.RESPONSE_YES:
name = _nd.display(spouse)
gid = spouse.get_gramps_id()
self.model.append([name, gid])
def delPersonClicked(self, obj):
(path, column) = self.treeView.get_cursor()
if (path):
iter = self.model.get_iter(path)
self.model.remove(iter)
#-------------------------------------------------------------------------
#
# Menu class

View File

@ -29,7 +29,7 @@
# of the list.
from _MenuOptions import MenuOptions, \
NumberOption, FloatOption, BooleanOption, TextOption, \
EnumeratedListOption, FilterListOption, StringOption
EnumeratedListOption, FilterListOption, StringOption, ColourButtonOption, PeoplePickerOption
from _PluginMgr import \
register_export, register_import, \
register_tool, register_report, \

View File

@ -104,7 +104,7 @@ else:
#-------------------------------------------------------------------------------
class GVDocBase(BaseDoc.BaseDoc,BaseDoc.GVDoc):
"""
Base document generator for all Graphiz codument generators. Classes that
Base document generator for all Graphiz document generators. Classes that
inherit from this class will only need to implement the close function.
The close function will generate the actual file of the appropriate type.
"""
@ -156,29 +156,29 @@ class GVDocBase(BaseDoc.BaseDoc,BaseDoc.GVDoc):
sizew = sizew * self.hpages
sizeh = sizeh * self.vpages
self.dot.write( 'digraph GRAMPS_graph\n' )
self.dot.write( '{\n' )
self.dot.write( ' bgcolor=white;\n' )
self.dot.write( ' center="true"; \n' )
self.dot.write( ' charset="iso-8859-1";\n' )
self.dot.write( ' concentrate="false";\n' )
self.dot.write( ' dpi="%d";\n' % self.dpi )
self.dot.write( ' graph [fontsize=%d];\n' % self.fontsize )
self.dot.write( ' mclimit="99";\n' )
self.dot.write( ' nodesep="%.2f";\n' % self.nodesep )
self.dot.write( ' outputorder="edgesfirst";\n' )
self.dot.write( ' page="%3.2f,%3.2f"; \n' % (pwidth, pheight) )
self.dot.write( ' pagedir="%s"; \n' % self.pagedir )
self.dot.write( ' rankdir="%s"; \n' % self.rankdir )
self.dot.write( ' ranksep="%.2f";\n' % self.ranksep )
self.dot.write( ' ratio="%s"; \n' % self.ratio )
self.dot.write( ' rotate="%d"; \n' % rotate )
self.dot.write( ' searchsize="100";\n' )
self.dot.write( ' size="%3.2f,%3.2f"; \n' % (sizew, sizeh) )
self.dot.write( ' splines="true";\n' )
self.dot.write( '\n' )
self.dot.write( 'digraph GRAMPS_graph\n' )
self.dot.write( '{\n' )
self.dot.write( ' bgcolor=white;\n' )
self.dot.write( ' center="true"; \n' )
self.dot.write( ' charset="iso-8859-1";\n' )
self.dot.write( ' concentrate="false";\n' )
self.dot.write( ' dpi="%d";\n' % self.dpi )
self.dot.write( ' graph [fontsize=%d];\n' % self.fontsize )
self.dot.write( ' mclimit="99";\n' )
self.dot.write( ' nodesep="%.2f";\n' % self.nodesep )
self.dot.write( ' outputorder="edgesfirst";\n' )
self.dot.write( ' page="%3.2f,%3.2f";\n' % (pwidth, pheight) )
self.dot.write( ' pagedir="%s";\n' % self.pagedir )
self.dot.write( ' rankdir="%s";\n' % self.rankdir )
self.dot.write( ' ranksep="%.2f";\n' % self.ranksep )
self.dot.write( ' ratio="%s";\n' % self.ratio )
self.dot.write( ' rotate="%d";\n' % rotate )
self.dot.write( ' searchsize="100";\n' )
self.dot.write( ' size="%3.2f,%3.2f"; \n' % (sizew, sizeh) )
self.dot.write( ' splines="true";\n' )
self.dot.write( '\n' )
self.dot.write( ' edge [len=0.5 style=solid arrowhead=none '
'arrowtail=normal fontsize=%d];\n' % self.fontsize )
'arrowtail=normal fontsize=%d];\n' % self.fontsize )
if self.fontfamily:
self.dot.write( ' node [style=filled fontname="%s" fontsize=%d];\n'
% ( self.fontfamily, self.fontsize ) )
@ -785,8 +785,12 @@ class GraphvizReportDialog(ReportDialog):
"in longer lines and larger graphs."))
self.options.add_menu_option(category, "usesubgraphs", usesubgraphs)
# this control only affects a subset of graphviz-based reports, so we
# need to remember it since we'll be toggling the visibility
self.usesubgraphs = usesubgraphs
################################
category = _("Notes")
category = _("Note")
################################
note = TextOption(_("Note to add to the graph"),
@ -808,12 +812,24 @@ class GraphvizReportDialog(ReportDialog):
self.options.load_previous_values()
def pages_changed(self, sp):
if self.v_pages.gobj.get_value_as_int() > 1 or \
self.h_pages.gobj.get_value_as_int() > 1:
# this method gets called every time the v_pages or h_pages
# spinbuttons are changed; when both vertical and horizontal
# pages are set to "1", then the page_dir control needs to
# be grayed out
if self.v_pages.gobj.get_value_as_int() > 1 or \
self.h_pages.gobj.get_value_as_int() > 1:
self.page_dir.combo.set_sensitive(True)
else:
self.page_dir.combo.set_sensitive(False)
def report_allows_subgraphs(self, state):
# if your report can take advantage of subgraphs, call this
# method to allow the users to toggle the state of subgraphs
if state:
self.usesubgraphs.gobj.show()
else:
self.usesubgraphs.gobj.hide()
def init_interface(self):
ReportDialog.init_interface(self)
self.doc_type_changed(self.format_menu)
@ -825,7 +841,11 @@ class GraphvizReportDialog(ReportDialog):
# number of horizontal and/or vertical pages is > 1
self.h_pages.gobj.connect('value-changed', self.pages_changed)
self.v_pages.gobj.connect('value-changed', self.pages_changed)
self.pages_changed(self.h_pages)
self.pages_changed(self.h_pages.gobj)
# note that the "use subgraph" option isn't used by many reports,
# so we'll hide it unless a reports specifically asks for it
self.report_allows_subgraphs(False)
def setup_format_frame(self):
"""Set up the format frame of the dialog."""

View File

@ -0,0 +1,839 @@
#
# Gramps - a GTK+/GNOME based genealogy program
#
# Copyright (C) 2007 Stephane Charette
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Pubilc 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$
"""
Family Lines, a GraphViz-based plugin for Gramps.
"""
#------------------------------------------------------------------------
#
# python modules
#
#------------------------------------------------------------------------
import os
import time
from gettext import gettext as _
#------------------------------------------------------------------------
#
# Set up logging
#
#------------------------------------------------------------------------
import logging
log = logging.getLogger(".FamilyLines")
#------------------------------------------------------------------------
#
# GNOME/gtk
#
#------------------------------------------------------------------------
import gtk
import gobject
#------------------------------------------------------------------------
#
# GRAMPS module
#
#------------------------------------------------------------------------
import gen.lib
import Config
import Errors
import Utils
import ThumbNails
import DateHandler
import GrampsWidgets
import ManagedWindow
from PluginUtils import register_report
from ReportBase import Report, ReportUtils, ReportOptions, CATEGORY_CODE, MODE_GUI, MODE_CLI
from ReportBase import Report, MenuReportOptions, MODE_GUI, MODE_CLI, CATEGORY_GRAPHVIZ
from ReportBase._ReportDialog import ReportDialog
from PluginUtils import register_report, FilterListOption, EnumeratedListOption, BooleanOption, NumberOption, ColourButtonOption, PeoplePickerOption
from QuestionDialog import ErrorDialog, WarningDialog
#from NameDisplay import displayer as _nd # Gramps version < 3.0
from BasicUtils import name_displayer as _nd # Gramps version >= 3.0
from DateHandler import displayer as _dd
from DateHandler import parser
from Selectors import selector_factory
#------------------------------------------------------------------------
#
# 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
# - see src/ReportBase/_ReportOptions.py for more information
#
# class FamilyLinesReport(Report)
# - this class is created only after the user clicks on "OK"
# - the actual report generation is done by this class
# - see src/ReportBase/_Report.py for more information
#
# Likely to be of additional interest is register_report() at the
# very bottom of this file.
#
#------------------------------------------------------------------------
class FamilyLinesOptions(MenuReportOptions):
"""
Defines all of the controls necessary
to configure the FamilyLines reports.
"""
def __init__(self, name, person_id=None):
MenuReportOptions.__init__(self, name, person_id)
def add_menu_options(self, menu):
# --------------------------------
category = _('People of Interest')
# --------------------------------
peoplePicker = PeoplePickerOption( _('People of interest'), '', None) # todo, fixme: need access to the database (3rd parm)
peoplePicker.set_help( _('People of interest are used as a starting point when determining \"family lines\".'))
menu.add_option(category, 'FLgidlist', peoplePicker)
followParents = BooleanOption( _('Follow parents to determine family lines'), True)
followParents.set_help( _('Parents and their ancestors will be considered when determining "family lines".'))
menu.add_option(category, 'FLfollowParents', followParents)
followChildren = BooleanOption( _('Follow children to determine family lines'), True)
followChildren.set_help( _('Children will be considered when determining "family lines".'))
menu.add_option(category, 'FLfollowChildren', followChildren)
removeExtraPeople = BooleanOption( _('Try to remove extra people and families'), True)
removeExtraPeople.set_help( _('People and families not directly related to people of interest will be removed when determining "family lines".'))
menu.add_option(category, 'FLremoveExtraPeople', removeExtraPeople)
# ----------------------------
category = _('Family Colours')
# ----------------------------
# todo, family colours
# -------------------------
category = _('Individuals')
# -------------------------
colourMales = ColourButtonOption( _('Males'), '#e0e0ff')
colourMales.set_help( _('The colour to use to display men.'))
menu.add_option(category, 'FLcolourMales', colourMales)
colourFemales = ColourButtonOption( _('Females'), '#ffe0e0')
colourFemales.set_help( _('The colour to use to display women.'))
menu.add_option(category, 'FLcolourFemales', colourFemales)
colourUnknown = ColourButtonOption( _('Unknown'), '#e0e0e0')
colourUnknown.set_help( _('The colour to use when the gender is unknown.'))
menu.add_option(category, 'FLcolourUnknown', colourUnknown)
colourFamily = ColourButtonOption( _('Families'), '#ffffe0')
colourFamily.set_help( _('The colour to use to display families.'))
menu.add_option(category, 'FLcolourFamilies', colourFamily)
limitParents = BooleanOption( _('Limit the number of parents'), False)
limitParents.set_help( _('The maximum number of ancestors to include.'))
menu.add_option(category, 'FLlimitParents', limitParents)
maxParents = NumberOption( '', 50, 10, 9999)
maxParents.set_help( _('The maximum number of ancestors to include.'))
menu.add_option(category, 'FLmaxParents', maxParents)
limitChildren = BooleanOption( _('Limit the number of children'), False)
limitChildren.set_help( _('The maximum number of children to include.'))
menu.add_option(category, 'FLlimitChildren', limitChildren)
maxChildren = NumberOption( '', 50, 10, 9999)
maxChildren.set_help( _('The maximum number of children to include.'))
menu.add_option(category, 'FLmaxChildren', maxChildren)
# --------------------
category = _('Images')
# --------------------
includeImages = BooleanOption( _('Include thumbnail images of people'), True)
includeImages.set_help( _('The maximum number of children to include.'))
menu.add_option(category, 'FLincludeImages', includeImages)
imageLocation = EnumeratedListOption(_('Thumbnail location'), 0)
imageLocation.add_item(0, _('Above the name'))
imageLocation.add_item(1, _('Beside the name'))
imageLocation.set_help( _('Where the thumbnail image should appear relative to the name'))
menu.add_option(category, 'FLimageOnTheSide', imageLocation)
# ---------------------
category = _('Options')
# ---------------------
includeDates = BooleanOption( _('Include dates'), True)
includeDates.set_help( _('Whether to include dates for people and families.'))
menu.add_option(category, 'FLincludeDates', includeDates)
includePlaces = BooleanOption( _('Include places'), True)
includePlaces.set_help( _('Whether to include placenames for people and families.'))
menu.add_option(category, 'FLincludePlaces', includePlaces)
includeNumChildren = BooleanOption( _('Include the number of children'), True)
includeNumChildren.set_help( _('Whether to include the number of children for families with more than 1 child.'))
menu.add_option(category, 'FLincludeNumChildren', includeNumChildren)
includeResearcher = BooleanOption( _('Include researcher and date'), True)
includeResearcher.set_help( _('Whether to include at the bottom the researcher''s name, e-mail, and the date the report was generated.'))
menu.add_option(category, 'FLincludeResearcher', includeResearcher)
includePrivate = BooleanOption( _('Include private records'), False)
includePrivate.set_help( _('Whether to include names, dates, and families that are marked as private.'))
menu.add_option(category, 'FLincludePrivate', includePrivate)
#------------------------------------------------------------------------
#
# FamilyLinesReport -- created once the user presses 'OK'
#
#------------------------------------------------------------------------
class FamilyLinesReport(Report):
def __init__(self, database, person, options):
"""
Creates FamilyLinesReport object that eventually produces the report.
The arguments are:
database - the GRAMPS database instance
person - currently selected person
options - instance of the FamilyLinesOptions class for this report
"""
# initialize several convenient variables
self.options = options
self.db = database
self.peopleToOutput = set() # handle of people we need in the report
self.familiesToOutput = set() # handle of families we need in the report
self.deletedPeople = 0
self.deletedFamilies = 0
# inherited from parent; see "usesubgraphs" in _GraphvizReportDialog.py
self.useSubgraphs = options.handler.options_dict['usesubgraphs' ]
# the remainder of the options are specific to this report
self.followParents = options.handler.options_dict['FLfollowParents' ]
self.followChildren = options.handler.options_dict['FLfollowChildren' ]
self.removeExtraPeople = options.handler.options_dict['FLremoveExtraPeople' ]
self.gidlist = options.handler.options_dict['FLgidlist' ]
self.colourMales = options.handler.options_dict['FLcolourMales' ]
self.colourFemales = options.handler.options_dict['FLcolourFemales' ]
self.colourUnknown = options.handler.options_dict['FLcolourUnknown' ]
self.colourFamilies = options.handler.options_dict['FLcolourFamilies' ]
self.limitParents = options.handler.options_dict['FLlimitParents' ]
self.maxParents = options.handler.options_dict['FLmaxParents' ]
self.limitChildren = options.handler.options_dict['FLlimitChildren' ]
self.maxChildren = options.handler.options_dict['FLmaxChildren' ]
self.includeImages = options.handler.options_dict['FLincludeImages' ]
self.imageOnTheSide = options.handler.options_dict['FLimageOnTheSide' ]
self.includeDates = options.handler.options_dict['FLincludeDates' ]
self.includePlaces = options.handler.options_dict['FLincludePlaces' ]
self.includeNumChildren = options.handler.options_dict['FLincludeNumChildren' ]
self.includeResearcher = options.handler.options_dict['FLincludeResearcher' ]
self.includePrivate = options.handler.options_dict['FLincludePrivate' ]
# 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.interestSet = set()
for gid in self.gidlist.split():
person = self.db.get_person_from_gramps_id(gid)
self.interestSet.add(person.get_handle())
# convert the 'surnameColours' string to a dictionary of names and colours
self.surnameColours = {}
tmp = '' # TODO, FIXME options.handler.options_dict['FLsurnameColours'].split()
while len(tmp) > 1:
surname = tmp.pop(0).encode('iso-8859-1','xmlcharrefreplace')
colour = tmp.pop(0)
self.surnameColours[surname] = colour
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
self.progress = Utils.ProgressMeter(_('Generate family lines'),_('Starting'))
# starting with the people of interest, we then add parents:
self.peopleToOutput.clear()
self.familiesToOutput.clear()
self.progress.set_pass(_('Finding ancestors and children'), self.db.get_number_of_people())
if self.followParents:
self.findParents()
if self.removeExtraPeople:
self.removeUninterestingParents()
# ...and/or with the people of interest we add their children:
if self.followChildren:
self.findChildren()
# 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
# since we know the exact number of people and families,
# we can then restart the progress bar with the exact
# number
self.progress.set_pass(_('Writing family lines'),
len(self.peopleToOutput ) + # every person needs to be written
len(self.familiesToOutput ) + # every family needs to be written
len(self.familiesToOutput )) # every family needs people assigned to it
# now that begin_report() has done the work, output what we've
# obtained into whatever file or format the user expects to use
self.writePeople()
self.writeFamilies()
self.progress.close()
def findParents(self):
# we need to start with all of our "people of interest"
ancestorsNotYetProcessed = set(self.interestSet)
# now we find all the immediate ancestors of our people of interest
while len(ancestorsNotYetProcessed) > 0:
handle = ancestorsNotYetProcessed.pop()
self.progress.step()
# One of 2 things can happen here:
# 1) we've 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.peopleToOutput:
person = self.db.get_person_from_handle(handle)
# if this is a private record, and we're not
# including private records, then go back to the
# top of the while loop to get the next person
if person.private and not self.includePrivate:
continue
# remember this person!
self.peopleToOutput.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 familyHandle in person.get_family_handle_list():
family = self.db.get_family_from_handle(familyHandle)
spouseHandle = ReportUtils.find_spouse(person, family)
if spouseHandle:
if spouseHandle in self.peopleToOutput or spouseHandle in ancestorsNotYetProcessed:
self.familiesToOutput.add(familyHandle)
# 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(ancestorsNotYetProcessed) + len(self.peopleToOutput))):
# 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 familyHandle in person.get_parent_family_handle_list():
family = self.db.get_family_from_handle(familyHandle)
if (family.private and self.includePrivate) or not family.private:
father = self.db.get_person_from_handle(family.get_father_handle())
mother = self.db.get_person_from_handle(family.get_mother_handle())
if father:
if (father.private and self.includePrivate) or not father.private:
ancestorsNotYetProcessed.add(family.get_father_handle())
self.familiesToOutput.add(familyHandle)
if mother:
if (mother.private and self.includePrivate) or not mother.private:
ancestorsNotYetProcessed.add(family.get_mother_handle())
self.familiesToOutput.add(familyHandle)
def removeUninterestingParents(self):
# start with all the people we've already identified
parentsNotYetProcessed = set(self.peopleToOutput)
while len(parentsNotYetProcessed) > 0:
handle = parentsNotYetProcessed.pop()
self.progress.step()
person = self.db.get_person_from_handle(handle)
# 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?
childHandle = None
numberOfChildren = 0
spouseHandle = None
numberOfSpouse = 0
fatherHandle = None
motherHandle = None
spouseFatherHandle = None
spouseMotherHandle = None
spouseSurname = ""
surname = person.get_primary_name().get_surname().encode('iso-8859-1','xmlcharrefreplace')
# first we get the person's father and mother
for familyHandle in person.get_parent_family_handle_list():
family = self.db.get_family_from_handle(familyHandle)
handle = family.get_father_handle()
if handle in self.peopleToOutput:
fatherHandle = handle
handle = family.get_mother_handle()
if handle in self.peopleToOutput:
motherHandle = handle
# now see how many spouses this person has
for familyHandle in person.get_family_handle_list():
family = self.db.get_family_from_handle(familyHandle)
handle = ReportUtils.find_spouse(person, family)
if handle in self.peopleToOutput:
numberOfSpouse += 1
spouse = self.db.get_person_from_handle(handle)
spouseHandle = handle
spouseSurname = spouse.get_primary_name().get_surname().encode('iso-8859-1','xmlcharrefreplace')
# see if the spouse has parents
if spouseFatherHandle == None and spouseMotherHandle == None:
for familyHandle in spouse.get_parent_family_handle_list():
family = self.db.get_family_from_handle(familyHandle)
handle = family.get_father_handle()
if handle in self.peopleToOutput:
spouseFatherHandle = handle
handle = family.get_mother_handle()
if handle in self.peopleToOutput:
spouseMotherHandle = handle
# get the number of children that we think might be interesting
for familyHandle in person.get_family_handle_list():
family = self.db.get_family_from_handle(familyHandle)
for childRef in family.get_child_ref_list():
if childRef.ref in self.peopleToOutput:
numberOfChildren += 1
childHandle = childRef.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 numberOfChildren > 1:
continue
# if this person has many spouses of interest, then we
# automatically keep this person
if numberOfSpouse > 1:
continue
# if this person has parents, then we automatically keep
# this person
if fatherHandle != None or motherHandle != None:
continue
# if the spouse has parents, then we automatically keep
# this person
if spouseFatherHandle != None or spouseMotherHandle != None:
continue;
# if this is a person of interest, then we automatically keep
if person.get_handle() in self.interestSet:
continue;
# if the spouse is a person of interest, then we keep
if spouseHandle in self.interestSet:
continue
# if the surname (or the spouse's surname) matches a person
# of interest, then we automatically keep this person
bKeepThisPerson = False
for personOfInterestHandle in self.interestSet:
personOfInterest = self.db.get_person_from_handle(personOfInterestHandle)
surnameOfInterest = personOfInterest.get_primary_name().get_surname().encode('iso-8859-1','xmlcharrefreplace')
if surnameOfInterest == surname or surnameOfInterest == spouseSurname:
bKeepThisPerson = True
break
if bKeepThisPerson:
continue
# if we have a special colour to use for this person,
# then we automatically keep this person
if surname in self.surnameColours:
continue
# if we have a special colour to use for the spouse,
# then we automatically keep this person
if spouseSurname in self.surnameColours:
continue
# took us a while, but if we get here, then we can remove this person
self.deletedPeople += 1
self.peopleToOutput.remove(person.get_handle())
# we can also remove any families to which this person belonged
for familyHandle in person.get_family_handle_list():
if familyHandle in self.familiesToOutput:
self.deletedFamilies += 1
self.familiesToOutput.remove(familyHandle)
# if we have a spouse, then ensure we queue up the spouse
if spouseHandle:
if spouseHandle not in parentsNotYetProcessed:
parentsNotYetProcessed.add(spouseHandle)
# if we have a child, then ensure we queue up the child
if childHandle:
if childHandle not in parentsNotYetProcessed:
parentsNotYetProcessed.add(childHandle)
def findChildren(self):
# we need to start with all of our "people of interest"
childrenNotYetProcessed = set(self.interestSet)
childrenToInclude = set()
# now we find all the children of our people of interest
while len(childrenNotYetProcessed) > 0:
handle = childrenNotYetProcessed.pop()
self.progress.step()
if handle not in childrenToInclude:
person = self.db.get_person_from_handle(handle)
# if this is a private record, and we're not
# including private records, then go back to the
# top of the while loop to get the next person
if person.private and not self.includePrivate:
continue
# remember this person!
childrenToInclude.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(childrenNotYetProcessed) + len(childrenToInclude))):
# 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 familyHandle in person.get_family_handle_list():
family = self.db.get_family_from_handle(familyHandle)
if (family.private and self.includePrivate) or not family.private:
# 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)
if (child.private and self.includePrivate) or not child.private:
childrenNotYetProcessed.add(child.get_handle())
self.familiesToOutput.add(familyHandle)
# include the spouse from this person's family
spouseHandle = ReportUtils.find_spouse(person, family)
if spouseHandle:
spouse = self.db.get_person_from_handle(spouseHandle)
if (spouse.private and self.includePrivate) or not spouse.private:
childrenToInclude.add(spouseHandle)
self.familiesToOutput.add(familyHandle)
# we now merge our temp set "childrenToInclude" into our master set
self.peopleToOutput.update(childrenToInclude)
def writePeople(self):
# if we're going to attempt to include images, then use the HTML style of .dot file
bUseHtmlOutput = False
if self.includeImages:
bUseHtmlOutput = True
# loop through all the people we need to output
for handle in self.peopleToOutput:
self.progress.step()
person = self.db.get_person_from_handle(handle)
name = person.get_primary_name().get_regular_name()
# figure out what colour to use
colour = self.colourUnknown
if person.get_gender() == gen.lib.Person.MALE:
colour = self.colourMales
if person.get_gender() == gen.lib.Person.FEMALE:
colour = self.colourFemales
# see if we have surname colours that match this person
surname = person.get_primary_name().get_surname().encode('iso-8859-1','xmlcharrefreplace')
if surname in self.surnameColours:
colour = self.surnameColours[surname]
# see if we have a birth date we can use
birthStr = None
if self.includeDates and person.get_birth_ref():
event = self.db.get_event_from_handle(person.get_birth_ref().ref)
if (event.private and self.includePrivate) or not event.private:
date = event.get_date_object()
if date.get_day_valid() and date.get_month_valid() and date.get_year_valid():
birthStr = _dd.display(date)
elif date.get_year_valid():
birthStr = '%d' % date.get_year()
# see if we have a birth place (one of: city, state, or country) we can use
birthplace = None
if self.includePlaces and person.get_birth_ref():
event = self.db.get_event_from_handle(person.get_birth_ref().ref)
if (event.private and self.includePrivate) or not event.private:
place = self.db.get_place_from_handle(event.get_place_handle())
if place:
location = place.get_main_location()
if location.get_city:
birthplace = location.get_city()
elif location.get_state:
birthplace = location.get_state()
elif location.get_country:
birthplace = location.get_country()
# see if we have a deceased date we can use
deathStr = None
if self.includeDates and person.get_death_ref():
event = self.db.get_event_from_handle(person.get_death_ref().ref)
if (event.private and self.includePrivate) or not event.private:
date = event.get_date_object()
if date.get_day_valid() and date.get_month_valid() and date.get_year_valid():
deathStr = _dd.display(date)
elif date.get_year_valid():
deathStr = '%d' % date.get_year()
# see if we have a place of death (one of: city, state, or country) we can use
deathplace = None
if self.includePlaces and person.get_death_ref():
event = self.db.get_event_from_handle(person.get_death_ref().ref)
if (event.private and self.includePrivate) or not event.private:
place = self.db.get_place_from_handle(event.get_place_handle())
if place:
location = place.get_main_location()
if location.get_city:
deathplace = location.get_city()
elif location.get_state:
deathplace = location.get_state()
elif location.get_country:
deathplace = location.get_country()
# see if we have an image to use for this person
imagePath = None
if self.includeImages:
mediaList = person.get_media_list()
if len(mediaList) > 0:
mediaHandle = mediaList[0].get_reference_handle()
media = self.db.get_object_from_handle(mediaHandle)
mediaMimeType = media.get_mime_type()
if mediaMimeType[0:5] == "image":
imagePath = ThumbNails.get_thumbnail_path(media.get_path())
# put the label together and ouput this person
label = u""
lineDelimiter = '\\n'
if bUseHtmlOutput:
lineDelimiter = '<BR/>'
# if we have an image, then start an HTML table; remember to close the table afterwards!
if imagePath:
label = u'<TABLE BORDER="0" CELLSPACING="2" CELLPADDING="0" CELLBORDER="0"><TR><TD><IMG SRC="%s"/></TD>' % imagePath
if self.imageOnTheSide == 0:
label += u'</TR><TR>'
label += '<TD>'
# at the very least, the label must have the person's name
label += name
if birthStr or deathStr:
label += ' %s(' % lineDelimiter
if birthStr:
label += '%s' % birthStr
label += ' - '
if deathStr:
label += '%s' % deathStr
label += ')'
if birthplace or deathplace:
if birthplace == deathplace:
deathplace = None # no need to print the same name twice
label += ' %s' % lineDelimiter
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 imagePath:
label += '</TD></TR></TABLE>'
if bUseHtmlOutput:
label = '<%s>' % label
else:
label = '"%s"' % label
self.write(' %s [shape="box", fillcolor="%s", label=%s];\n' % (person.get_gramps_id(), colour, label))
def writeFamilies(self):
# loop through all the families we need to output
for familyHandle in self.familiesToOutput:
self.progress.step()
family = self.db.get_family_from_handle(familyHandle)
fgid = family.get_gramps_id()
# figure out a wedding date or placename we can use
weddingDate = None
weddingPlace = None
if self.includeDates or self.includePlaces:
for event_ref in family.get_event_ref_list():
event = self.db.get_event_from_handle(event_ref.ref)
if event.get_type() == gen.lib.EventType.MARRIAGE:
# get the wedding date
if (event.private and self.includePrivate) or not event.private:
if self.includeDates:
date = event.get_date_object()
if date.get_day_valid() and date.get_month_valid() and date.get_year_valid():
weddingDate = _dd.display(date)
elif date.get_year_valid():
weddingDate = '%d' % date.get_year()
# get the wedding location
if self.includePlaces:
place = self.db.get_place_from_handle(event.get_place_handle())
if place:
location = place.get_main_location()
if location.get_city:
weddingPlace = location.get_city()
elif location.get_state:
weddingPlace = location.get_state()
elif location.get_country:
weddingPlace = location.get_country()
break
# figure out the number of children (if any)
childrenStr = None
if self.includeNumChildren:
numberOfChildren = len(family.get_child_ref_list())
# if numberOfChildren == 1:
# childrenStr = _('1 child')
if numberOfChildren > 1:
childrenStr = _('%d children') % numberOfChildren
label = ''
if weddingDate:
if label != '':
label += '\\n'
label += '%s' % weddingDate
if weddingPlace:
if label != '':
label += '\\n'
label += '%s' % weddingPlace
if childrenStr:
if label != '':
label += '\\n'
label += '%s' % childrenStr
self.write(' %s [shape="ellipse", fillcolor="%s", label="%s"];\n' % (fgid, self.colourFamilies, label))
# now that we have the families written, go ahead and link the parents and children to the families
for familyHandle in self.familiesToOutput:
self.progress.step()
self.write('\n')
# get the parents for this family
family = self.db.get_family_from_handle(familyHandle)
fgid = family.get_gramps_id()
fatherHandle = family.get_father_handle()
motherHandle = family.get_mother_handle()
if self.useSubgraphs and fatherHandle and motherHandle:
self.write(' subgraph cluster_%s\n' % fgid)
self.write(' {\n')
# see if we have a father to link to this family
if fatherHandle:
if fatherHandle in self.peopleToOutput:
father = self.db.get_person_from_handle(fatherHandle)
self.write(' %s -> %s // father: %s\n' % (fgid, father.get_gramps_id(), father.get_primary_name().get_regular_name()))
# see if we have a mother to link to this family
if motherHandle:
if motherHandle in self.peopleToOutput:
mother = self.db.get_person_from_handle(motherHandle)
self.write(' %s -> %s // mother: %s\n' % (fgid, mother.get_gramps_id(), mother.get_primary_name().get_regular_name()))
if self.useSubgraphs and fatherHandle and motherHandle:
self.write(' }\n')
# link the children to the family
for childRef in family.get_child_ref_list():
if childRef.ref in self.peopleToOutput:
child = self.db.get_person_from_handle(childRef.ref)
self.write(' %s -> %s // child: %s\n' % (child.get_gramps_id(), fgid, child.get_primary_name().get_regular_name()))
#------------------------------------------------------------------------
#
# register_report() is defined in _PluginMgr.py and
# is used to hook the plugin into GRAMPS so that it
# appears in the "Reports" menu options
#
#------------------------------------------------------------------------
register_report(
name = 'familylines_graph',
category = CATEGORY_GRAPHVIZ,
report_class = FamilyLinesReport, # must implement write_report(), called by report() in _ReportDialog.py
options_class = FamilyLinesOptions, # must implement add_menu_options(), called by MenuOptions::__init__()
modes = MODE_GUI,
status = _("Stable"),
translated_name = _("Family Lines Graph"),
author_name = "Stephane Charette",
author_email = "stephanecharette@gmail.com",
description =_("Generates family line graphs using GraphViz."),
)

View File

@ -304,7 +304,14 @@ class RelGraphReport(Report):
label = u""
lineDelimiter = '\\n'
# if we have an image, then start an HTML table; remember to close the table afterwards!
# If we have an image, then start an HTML table; remember to close the table afterwards!
#
# This isn't a free-form HTML format here...just a few keywords that happen to be
# simillar to keywords commonly seen in HTML. For additional information on what
# is allowed, see:
#
# http://www.graphviz.org/info/shapes.html#html
#
if self.bUseHtmlOutput and imagePath:
lineDelimiter = '<BR/>'
label += '<TABLE BORDER="0" CELLSPACING="2" CELLPADDING="0" CELLBORDER="0"><TR><TD></TD><TD><IMG SRC="%s"/></TD><TD></TD>' % imagePath
@ -503,4 +510,5 @@ register_report(
description = _("Generates a relationship graphs using Graphviz."),
author_name ="Brian G. Matherly",
author_email ="brian@gramps-project.org"
)
)

View File

@ -37,6 +37,7 @@ pkgdata_PYTHON = \
FanChart.py\
FindDupes.py\
GraphViz.py\
GVFamilyLines.py \
GVHourGlass.py\
GVRelGraph.py\
ImportCSV.py\