diff --git a/ChangeLog b/ChangeLog index 4673e3f6e..6d78a0ff6 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,3 +1,15 @@ +2007-12-28 Stéphane Charette + * 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 * src/plugins/DetDescendantReport.py: * src/plugins/DetAncestralReport.py: diff --git a/po/POTFILES.in b/po/POTFILES.in index 60bceb292..4f0345945 100644 --- a/po/POTFILES.in +++ b/po/POTFILES.in @@ -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 diff --git a/src/PluginUtils/_MenuOptions.py b/src/PluginUtils/_MenuOptions.py index 344e37335..edbe1900e 100644 --- a/src/PluginUtils/_MenuOptions.py +++ b/src/PluginUtils/_MenuOptions.py @@ -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 diff --git a/src/PluginUtils/__init__.py b/src/PluginUtils/__init__.py index f4d526536..afaafbd0b 100644 --- a/src/PluginUtils/__init__.py +++ b/src/PluginUtils/__init__.py @@ -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, \ diff --git a/src/ReportBase/_GraphvizReportDialog.py b/src/ReportBase/_GraphvizReportDialog.py index 43d0cfe61..7c26e1316 100644 --- a/src/ReportBase/_GraphvizReportDialog.py +++ b/src/ReportBase/_GraphvizReportDialog.py @@ -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.""" diff --git a/src/plugins/GVFamilyLines.py b/src/plugins/GVFamilyLines.py new file mode 100644 index 000000000..03ee991ae --- /dev/null +++ b/src/plugins/GVFamilyLines.py @@ -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 = '
' + + # if we have an image, then start an HTML table; remember to close the table afterwards! + if imagePath: + label = u'' % imagePath + if self.imageOnTheSide == 0: + label += u'' + label += '
' + + # 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 += '
' + + 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."), + ) + diff --git a/src/plugins/GVRelGraph.py b/src/plugins/GVRelGraph.py index d4dfb3abc..948d8b710 100644 --- a/src/plugins/GVRelGraph.py +++ b/src/plugins/GVRelGraph.py @@ -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 = '
' label += '' % 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" - ) \ No newline at end of file + ) + diff --git a/src/plugins/Makefile.am b/src/plugins/Makefile.am index bfec47d0d..6ee401af1 100644 --- a/src/plugins/Makefile.am +++ b/src/plugins/Makefile.am @@ -37,6 +37,7 @@ pkgdata_PYTHON = \ FanChart.py\ FindDupes.py\ GraphViz.py\ + GVFamilyLines.py \ GVHourGlass.py\ GVRelGraph.py\ ImportCSV.py\