diff --git a/po/POTFILES.in b/po/POTFILES.in
index b6678520e..51139bbaa 100644
--- a/po/POTFILES.in
+++ b/po/POTFILES.in
@@ -299,6 +299,7 @@ src/plugins/ExportVCalendar.py
src/plugins/ExportVCard.py
src/plugins/ExtractCity.py
src/plugins/FamilyGroup.py
+src/plugins/FamilyLines.py
src/plugins/FanChart.py
src/plugins/GraphViz.py
src/plugins/ImportGeneWeb.py
diff --git a/src/plugins/FamilyLines.py b/src/plugins/FamilyLines.py
new file mode 100644
index 000000000..a7ba100cc
--- /dev/null
+++ b/src/plugins/FamilyLines.py
@@ -0,0 +1,1432 @@
+#
+# 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 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 RelLib
+import Config
+import Errors
+import Utils
+import ImgManip
+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._ReportDialog import ReportDialog
+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
+
+#------------------------------------------------------------------------
+#
+# FamilyLinesReport -- created once the user presses 'OK' to actually
+# go ahead and create the full report
+#
+#------------------------------------------------------------------------
+class FamilyLinesReport(Report):
+ def __init__(self, database, person, options):
+ """
+ Creates FamilyLinesReport object that produces the report.
+
+ The arguments are:
+
+ database - the GRAMPS database instance
+ person - currently selected person
+ options - instance of the Options class for this report
+ """
+
+ self.start_person = person
+ 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
+
+ self.filename = options.handler.options_dict['FLfilename' ]
+ self.width = options.handler.options_dict['FLwidth' ]
+ self.height = options.handler.options_dict['FLheight' ]
+ self.dpi = options.handler.options_dict['FLdpi' ]
+ self.rowSep = options.handler.options_dict['FLrowSep' ]
+ self.colSep = options.handler.options_dict['FLcolSep' ]
+ self.direction = options.handler.options_dict['FLdirection' ]
+ self.ratio = options.handler.options_dict['FLratio' ]
+ 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 = 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 write(self, text):
+# self.of.write(text.encode('iso-8859-1', 'strict'))
+ self.of.write(text.encode('iso-8859-1','xmlcharrefreplace'))
+
+
+ def writeDotHeader(self):
+ self.write('# Researcher: %s\n' % Config.get(Config.RESEARCHER_NAME))
+ self.write('# Generated on %s\n' % time.strftime('%c') )
+ self.write('# Number of people in database: %d\n' % self.db.get_number_of_people())
+ self.write('# Number of people of interest: %d\n' % len(self.peopleToOutput))
+ self.write('# Number of families in database: %d\n' % self.db.get_number_of_families())
+ self.write('# Number of families of interest: %d\n' % len(self.familiesToOutput))
+
+ if self.removeExtraPeople:
+ self.write('# Additional people removed: %d\n' % self.deletedPeople)
+ self.write('# Additional families removed: %d\n' % self.deletedFamilies)
+
+ self.write('# Initial list of people of interest:\n')
+ for handle in self.interestSet:
+ person = self.db.get_person_from_handle(handle)
+ name = person.get_primary_name().get_regular_name()
+ self.write('# -> %s\n' % name)
+ self.write('\n')
+
+ if self.limitParents:
+ self.write('# NOTE: Option has been set to limit the output to %d parents.\n' % self.maxParents)
+ self.write('\n')
+
+ if self.limitParents:
+ self.write('# NOTE: Option has been set to limit the output to %d children.\n' % self.maxChildren)
+ self.write('\n')
+
+ self.write('digraph FamilyLines\n' )
+ self.write('{\n' )
+ self.write(' bgcolor="white";\n' )
+ self.write(' center="true";\n' )
+ self.write(' charset="iso-8859-1";\n' )
+ self.write(' concentrate="false";\n' )
+ self.write(' dpi="%d";\n' % self.dpi )
+ self.write(' graph [fontsize=12];\n' )
+ self.write(' mclimit="99";\n' )
+ self.write(' nodesep="%.2f";\n' % self.rowSep )
+ self.write(' outputorder="edgesfirst";\n' )
+ self.write(' page="%.2f,%.2f";\n' % (self.width, self.height) )
+ self.write('# pagedir="BL";\n' )
+ self.write(' rankdir="%s";\n' % self.direction )
+ self.write(' ranksep="%.2f";\n' % self.colSep )
+ self.write(' ratio="%s";\n' % self.ratio )
+ self.write(' rotate="0";\n' )
+ self.write(' searchsize="100";\n' )
+ self.write(' size="%.2f,%.2f";\n' % (self.width, self.height) )
+ self.write(' splines="true";\n' )
+ self.write('\n' )
+ self.write(' edge [len=0.5 style=solid arrowhead=none arrowtail=normal fontsize=12];\n')
+ self.write(' node [style=filled fontname="FreeSans" fontsize=12];\n' )
+ self.write('\n' )
+
+
+ def writeDotFooter(self):
+ if self.includeResearcher:
+ name = Config.get(Config.RESEARCHER_NAME)
+ email = Config.get(Config.RESEARCHER_EMAIL)
+ date = DateHandler.parser.parse(time.strftime('%b %d %Y'))
+ label = ''
+ if name:
+ label += '%s\\n' % name
+ if email:
+ label += '%s\\n' % email
+ label += '%s' % _dd.display(date)
+ self.write('\n')
+ self.write(' labelloc="b";\n')
+ self.write(' label="%s";\n' % label)
+
+ self.write('}\n')
+
+
+ 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() == RelLib.Person.MALE:
+ colour = self.colourMales
+ if person.get_gender() == RelLib.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 = os.path.abspath(ImgManip.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' |
' + + # 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 += ' |