e0543b0644
svn: r9634
1441 lines
69 KiB
Python
1441 lines
69 KiB
Python
#
|
|
# 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 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._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.useSubgraphs = options.handler.options_dict['FLuseSubgraphs' ]
|
|
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() == 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()))
|
|
|
|
|
|
def write_report(self):
|
|
|
|
# see if we're going to have problems writing the file
|
|
if os.path.isdir(self.filename):
|
|
ErrorDialog(_('Invalid file name'), _('The archive file must be a file, not a directory'))
|
|
return
|
|
|
|
try:
|
|
self.of = open(self.filename, "w")
|
|
except (OSError,IOError),value:
|
|
ErrorDialog(_("Could not create %s") % self.filename)
|
|
return
|
|
|
|
self.progress = Utils.ProgressMeter(_('Generate family lines'),_('Starting'))
|
|
|
|
# starting with the people of interest, we then add parents and children:
|
|
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 we add children:
|
|
if self.followChildren:
|
|
self.findChildren()
|
|
|
|
# write out the report now that we know who we want
|
|
|
|
# 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
|
|
|
|
self.writeDotHeader()
|
|
self.writePeople()
|
|
self.writeFamilies()
|
|
self.writeDotFooter()
|
|
self.of.close()
|
|
self.progress.close()
|
|
|
|
|
|
#------------------------------------------------------------------------
|
|
#
|
|
# Create all of the GUI controls that we're going to need.
|
|
# (...and setup the default values for all those GUI controls...)
|
|
#
|
|
#------------------------------------------------------------------------
|
|
class FamilyLinesOptions(ReportOptions):
|
|
|
|
"""
|
|
Defines options and provides handling interface.
|
|
"""
|
|
|
|
def __init__(self, name, dialog):
|
|
ReportOptions.__init__(self, name, None)
|
|
self.dialog = dialog
|
|
|
|
# Options specific for this report
|
|
self.options_dict = {
|
|
'FLfilename' : 'familylines.dot',
|
|
'FLwidth' : 48.00,
|
|
'FLheight' : 36.00,
|
|
'FLdpi' : 75,
|
|
'FLrowSep' : 0.20,
|
|
'FLcolSep' : 0.20,
|
|
'FLdirection' : 'RL',
|
|
'FLratio' : 'compress',
|
|
'FLuseSubgraphs' : 0,
|
|
'FLfollowParents' : 0,
|
|
'FLfollowChildren' : 0,
|
|
'FLremoveExtraPeople' : 1,
|
|
'FLgidlist' : '',
|
|
'FLcolourMales' : '#e0e0ff', # blue
|
|
'FLcolourFemales' : '#ffe0e0', # pink
|
|
'FLcolourUnknown' : '#e0e0e0', # gray
|
|
'FLcolourFamilies' : '#ffffe0', # yellow
|
|
'FLsurnameColours' : '',
|
|
'FLlimitParents' : 0,
|
|
'FLmaxParents' : 75,
|
|
'FLlimitChildren' : 0,
|
|
'FLmaxChildren' : 75,
|
|
'FLincludeImages' : 1,
|
|
'FLimageOnTheSide' : 1,
|
|
'FLincludeDates' : 1,
|
|
'FLincludePlaces' : 1,
|
|
'FLincludeNumChildren' : 1,
|
|
'FLincludeResearcher' : 1,
|
|
'FLincludePrivate' : 0
|
|
}
|
|
|
|
# self.options_help = {
|
|
# }
|
|
|
|
# def enable_options(self):
|
|
# # Semi-common options that should be enabled for this report
|
|
# self.enable_dict = {
|
|
# }
|
|
|
|
def add_user_options(self, dialog):
|
|
"""called from base class to allow us the opportunity to create some UI controls"""
|
|
|
|
# self.dialog.target_fileentry.set_filename(self.options_dict['FLfilename'])
|
|
|
|
# ******** GRAPHVIZ OPTIONS **********
|
|
title = _("GraphViz Options")
|
|
widthAdj = gtk.Adjustment(value=self.options_dict['FLwidth' ], lower=8.00, upper=1000.00, step_incr=0.25)
|
|
heightAdj = gtk.Adjustment(value=self.options_dict['FLheight' ], lower=8.00, upper=1000.00, step_incr=0.25)
|
|
dpiAdj = gtk.Adjustment(value=self.options_dict['FLdpi' ], lower=20, upper=1200, step_incr=1)
|
|
rowSepAdj = gtk.Adjustment(value=self.options_dict['FLrowSep' ], lower=0.01, upper=5, step_incr=0.01)
|
|
colSepAdj = gtk.Adjustment(value=self.options_dict['FLcolSep' ], lower=0.01, upper=5, step_incr=0.01)
|
|
|
|
self.width = gtk.SpinButton(adjustment=widthAdj, digits=2)
|
|
self.height = gtk.SpinButton(adjustment=heightAdj, digits=2)
|
|
self.dpi = gtk.SpinButton(adjustment=dpiAdj, digits=0)
|
|
self.rowSep = gtk.SpinButton(adjustment=rowSepAdj, digits=2)
|
|
self.colSep = gtk.SpinButton(adjustment=colSepAdj, digits=2)
|
|
|
|
self.direction = gtk.combo_box_new_text()
|
|
self.ratio = gtk.combo_box_new_text()
|
|
|
|
direction_options = [_('left to right'), _('right to left'), _('top to bottom'), _('bottom to top')]
|
|
for text in direction_options:
|
|
self.direction.append_text(text)
|
|
direction_text = self.options_dict['FLdirection']
|
|
if direction_text == 'LR':
|
|
self.direction.set_active(direction_options.index(_('left to right')))
|
|
elif direction_text == 'RL':
|
|
self.direction.set_active(direction_options.index(_('right to left')))
|
|
elif direction_text == 'TB':
|
|
self.direction.set_active(direction_options.index(_('top to bottom')))
|
|
else:
|
|
self.direction.set_active(direction_options.index(_('bottom to top')))
|
|
|
|
ratio_options = ['auto', 'compress', 'expand', 'fill']
|
|
for text in ratio_options:
|
|
self.ratio.append_text(text)
|
|
ratio_text = self.options_dict['FLratio']
|
|
if ratio_text in ratio_options:
|
|
self.ratio.set_active(ratio_options.index(ratio_text))
|
|
else:
|
|
self.ratio.set_active(0)
|
|
|
|
self.useSubgraphs = gtk.CheckButton(_("Use subgraphs to display spouses closer together"))
|
|
self.useSubgraphs.set_active(self.options_dict['FLuseSubgraphs'])
|
|
|
|
dialog.add_frame_option(title, _('Width' ), self.width , _('Width of the graph in inches. Final image size may be smaller than this if ratio type is "Compress".'))
|
|
dialog.add_frame_option(title, _('Height' ), self.height , _('Height of the graph in inches. Final image size may be smaller than this if ratio type is "Compress".'))
|
|
dialog.add_frame_option(title, _('DPI' ), self.dpi , _('Dots per inch. When planning to create .gif or .png files for the web, try numbers such as 75 or 100 DPI.'))
|
|
dialog.add_frame_option(title, _('Row spacing' ), self.rowSep , _('The minimum amount of free space, in inches, between individual rows.'))
|
|
dialog.add_frame_option(title, _('Columns spacing' ), self.colSep , _('The minimum amount of free space, in inches, between individual columns.'))
|
|
dialog.add_frame_option(title, _('Graph direction' ), self.direction , _('Left-to-right means oldest ancestors on the left, youngest on the right. Top-to-bottom means oldest ancestors on the top, youngest on the botom.'))
|
|
dialog.add_frame_option(title, _('Ratio' ), self.ratio , _('See the GraphViz documentation for details on the use of "ratio". '))
|
|
dialog.add_frame_option(title, None, self.useSubgraphs, _('Subgraphs can help GraphViz position spouses closer together, but can also cause longer lines and larger graphs.'))
|
|
|
|
# ******** PEOPLE OF INTEREST **********
|
|
title = _("People of Interest")
|
|
|
|
# build up a container to display all of the people of interest
|
|
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)
|
|
|
|
for gid in self.options_dict['FLgidlist'].split():
|
|
person = self.dialog.database.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.dialog.addPersonClicked)
|
|
self.delPerson = GrampsWidgets.SimpleButton(gtk.STOCK_REMOVE, self.dialog.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)
|
|
|
|
self.followParents = gtk.CheckButton(_("Follow parents to determine family lines"))
|
|
self.followChildren = gtk.CheckButton(_("Follow children to determine family lines"))
|
|
self.removeExtraPeople = gtk.CheckButton(_("Try to remove extra people and families"))
|
|
|
|
self.followParents.set_active(self.options_dict['FLfollowParents'])
|
|
self.followChildren.set_active(self.options_dict['FLfollowChildren'])
|
|
self.removeExtraPeople.set_active(self.options_dict['FLremoveExtraPeople'])
|
|
|
|
dialog.add_frame_option(title, _('People\nof\ninterest'), self.hbox, _('People of interest are used as a starting point when determining \"family lines\".'))
|
|
dialog.add_frame_option(title, None, self.followParents, _('Parents and their ancestors will be considered when determining "family lines".'))
|
|
dialog.add_frame_option(title, None, self.followChildren, _('Children will be considered when determining "family lines".'))
|
|
dialog.add_frame_option(title, None, self.removeExtraPeople, _('People and families not directly related to people of interest will be removed when determining "family lines".'))
|
|
|
|
# ******** FAMILY COLOURS **********
|
|
title = _("Family Colours")
|
|
|
|
self.familyLinesModel = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING)
|
|
self.familyLinesTreeView = gtk.TreeView(self.familyLinesModel)
|
|
self.familyLinesTreeView.set_size_request(150, 150)
|
|
self.familyLinesTreeView.connect('row-activated', self.dialog.familyLinesClicked)
|
|
col1 = gtk.TreeViewColumn(_('Surname'), gtk.CellRendererText(), text=0)
|
|
col2 = gtk.TreeViewColumn(_('Colour'), gtk.CellRendererText(), text=1)
|
|
col1.set_resizable(True)
|
|
col2.set_resizable(True)
|
|
col1.set_sort_column_id(0)
|
|
col1.set_sizing(gtk.TREE_VIEW_COLUMN_AUTOSIZE)
|
|
col2.set_sizing(gtk.TREE_VIEW_COLUMN_AUTOSIZE)
|
|
self.familyLinesTreeView.append_column(col1)
|
|
self.familyLinesTreeView.append_column(col2)
|
|
self.familyLinesScrolledWindow = gtk.ScrolledWindow()
|
|
self.familyLinesScrolledWindow.add(self.familyLinesTreeView)
|
|
self.familyLinesScrolledWindow.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
|
|
self.familyLinesScrolledWindow.set_shadow_type(gtk.SHADOW_OUT)
|
|
self.familyLinesHbox = gtk.HBox()
|
|
self.familyLinesHbox.pack_start(self.familyLinesScrolledWindow, expand=True, fill=True)
|
|
|
|
self.addSurname = GrampsWidgets.SimpleButton(gtk.STOCK_ADD, self.dialog.addSurnameClicked)
|
|
self.delSurname = GrampsWidgets.SimpleButton(gtk.STOCK_REMOVE, self.dialog.delSurnameClicked)
|
|
self.familyLinesVbbox = gtk.VButtonBox()
|
|
self.familyLinesVbbox.add(self.addSurname)
|
|
self.familyLinesVbbox.add(self.delSurname)
|
|
self.familyLinesVbbox.set_layout(gtk.BUTTONBOX_SPREAD)
|
|
self.familyLinesHbox.pack_end(self.familyLinesVbbox, expand=False)
|
|
|
|
dialog.add_frame_option(title, None, self.familyLinesHbox)
|
|
|
|
# populate the surname/colour treeview
|
|
tmp = self.options_dict['FLsurnameColours'].split()
|
|
while len(tmp) > 1:
|
|
surname = tmp.pop(0)
|
|
colour = tmp.pop(0)
|
|
self.familyLinesModel.append([surname, colour])
|
|
|
|
# ******** INDIVIDUALS **********
|
|
title = _("Individuals")
|
|
self.colourMales = gtk.ColorButton(gtk.gdk.color_parse(self.options_dict['FLcolourMales']))
|
|
self.colourFemales = gtk.ColorButton(gtk.gdk.color_parse(self.options_dict['FLcolourFemales']))
|
|
self.colourUnknown = gtk.ColorButton(gtk.gdk.color_parse(self.options_dict['FLcolourUnknown']))
|
|
self.colourFamilies = gtk.ColorButton(gtk.gdk.color_parse(self.options_dict['FLcolourFamilies']))
|
|
|
|
self.limitParents = gtk.CheckButton(_('Limit the number of parents'))
|
|
maxParentsAdj = gtk.Adjustment(value=self.options_dict['FLmaxParents' ], lower=10, upper=9999, step_incr=1)
|
|
self.maxParents = gtk.SpinButton(adjustment=maxParentsAdj, digits=0)
|
|
self.limitChildren = gtk.CheckButton(_('Limit the number of children'))
|
|
maxChildrenAdj = gtk.Adjustment(value=self.options_dict['FLmaxChildren' ], lower=10, upper=9999, step_incr=1)
|
|
self.maxChildren = gtk.SpinButton(adjustment=maxChildrenAdj, digits=0)
|
|
self.limitParents.set_active(self.options_dict['FLlimitParents'])
|
|
self.limitChildren.set_active(self.options_dict['FLlimitChildren'])
|
|
|
|
dialog.add_frame_option(title, _('Males'), self.colourMales)
|
|
dialog.add_frame_option(title, _('Females'), self.colourFemales)
|
|
dialog.add_frame_option(title, _('Unknown'), self.colourUnknown)
|
|
dialog.add_frame_option(title, _('Families'), self.colourFamilies)
|
|
dialog.add_frame_option(title, None, self.limitParents)
|
|
dialog.add_frame_option(title, None, self.maxParents, _('The maximum number of ancestors to include.'))
|
|
dialog.add_frame_option(title, None, self.limitChildren)
|
|
dialog.add_frame_option(title, None, self.maxChildren, _('The maximum number of children to include.'))
|
|
|
|
# ******** IMAGES ********
|
|
title = _("Images")
|
|
self.includeImages = gtk.CheckButton(_('Include thumbnail images of people'))
|
|
self.imageLocation = gtk.combo_box_new_text()
|
|
self.imageLocation.append_text(_('place the thumbnail image above the name'))
|
|
self.imageLocation.append_text(_('place the thumbnail image beside the name'))
|
|
|
|
self.includeImages.set_active(self.options_dict['FLincludeImages'])
|
|
self.imageLocation.set_active(self.options_dict['FLimageOnTheSide'])
|
|
|
|
dialog.add_frame_option(title, None, self.includeImages, _("Whether to include thumbnails of people."))
|
|
dialog.add_frame_option(title, None, self.imageLocation, _("Whether the thumbnails and the names are side-by-side, or one above the other."))
|
|
|
|
# ******** OPTIONS *********
|
|
title = _("Options")
|
|
self.includeDates = gtk.CheckButton(_('Include dates'))
|
|
self.includePlaces = gtk.CheckButton(_('Include places'))
|
|
self.includeNumChildren = gtk.CheckButton(_('Include the number of children'))
|
|
self.includeResearcher = gtk.CheckButton(_('Include researcher and date'))
|
|
self.includePrivate = gtk.CheckButton(_('Include private records'))
|
|
self.graphviz = gtk.Label(_(
|
|
'This report will generate a .dot format file which can then be '
|
|
'processed with the Graphviz package to generate various file '
|
|
'formats such as .pdf, .gif, .svg, and many others. Additional '
|
|
'Graphviz information is available from:\n'
|
|
' http://www.graphviz.org/\n'
|
|
'\n'
|
|
'Quick reference: a .png file can be created using:\n'
|
|
' dot -Tpng -oexample.png familylines.dot'))
|
|
self.graphviz.set_line_wrap(True)
|
|
self.graphviz.set_single_line_mode(False)
|
|
self.graphviz.set_selectable(True)
|
|
|
|
self.includeDates.set_active( self.options_dict['FLincludeDates' ])
|
|
self.includePlaces.set_active( self.options_dict['FLincludePlaces' ])
|
|
self.includeNumChildren.set_active( self.options_dict['FLincludeNumChildren'])
|
|
self.includeResearcher.set_active( self.options_dict['FLincludeResearcher' ])
|
|
self.includePrivate.set_active( self.options_dict['FLincludePrivate' ])
|
|
|
|
dialog.add_frame_option(title, None, self.includeDates, _("Whether to include dates for people and families." ))
|
|
dialog.add_frame_option(title, None, self.includePlaces, _("Whether to include placenames for people and families." ))
|
|
dialog.add_frame_option(title, None, self.includeNumChildren, _("Whether to include the number of children for families with more than 1 child." ))
|
|
dialog.add_frame_option(title, None, self.includeResearcher, _("Whether to include at the bottom the researcher's name, e-mail, and the date the report was generated."))
|
|
dialog.add_frame_option(title, None, self.includePrivate, _("Whether to include names, dates, and families that are considered private." ))
|
|
dialog.add_frame_option(title, None, self.graphviz)
|
|
|
|
self.includeImages.connect( 'toggled', self.toggled)
|
|
self.followParents.connect( 'toggled', self.toggled)
|
|
self.followChildren.connect('toggled', self.toggled)
|
|
self.limitParents.connect( 'toggled', self.toggled)
|
|
self.limitChildren.connect( 'toggled', self.toggled)
|
|
self.toggled(self.limitParents)
|
|
|
|
|
|
def parse_user_options(self,dialog):
|
|
# Save the user selected choices for later use.
|
|
filename = self.dialog.target_fileentry.get_full_path(0)
|
|
if os.path.isdir(filename):
|
|
if filename[-1:] != '/':
|
|
filename += '/'
|
|
filename += 'familylines.dot'
|
|
self.dialog.target_fileentry.set_filename(filename)
|
|
if filename[-4:] != '.dot':
|
|
filename += '.dot'
|
|
self.dialog.target_fileentry.set_filename(filename)
|
|
self.options_dict['FLfilename' ] = filename
|
|
self.options_dict['FLwidth' ] = self.width.get_value()
|
|
self.options_dict['FLheight' ] = self.height.get_value()
|
|
self.options_dict['FLdpi' ] = int(self.dpi.get_value())
|
|
self.options_dict['FLrowSep' ] = self.rowSep.get_value()
|
|
self.options_dict['FLcolSep' ] = self.colSep.get_value()
|
|
if self.direction.get_active_text() == _('left to right'):
|
|
self.options_dict['FLdirection' ] = 'LR'
|
|
elif self.direction.get_active_text() == _('right to left'):
|
|
self.options_dict['FLdirection' ] = 'RL'
|
|
elif self.direction.get_active_text() == _('top to bottom'):
|
|
self.options_dict['FLdirection' ] = 'TB'
|
|
else:
|
|
self.options_dict['FLdirection' ] = 'BT'
|
|
self.options_dict['FLratio' ] = self.ratio.get_active_text()
|
|
self.options_dict['FLuseSubgraphs' ] = int(self.useSubgraphs.get_active() )
|
|
self.options_dict['FLfollowParents' ] = int(self.followParents.get_active() )
|
|
self.options_dict['FLfollowChildren' ] = int(self.followChildren.get_active() )
|
|
self.options_dict['FLremoveExtraPeople' ] = int(self.removeExtraPeople.get_active() )
|
|
self.options_dict['FLlimitParents' ] = int(self.limitParents.get_active() )
|
|
self.options_dict['FLmaxParents' ] = int(self.maxParents.get_value() )
|
|
self.options_dict['FLlimitChildren' ] = int(self.limitChildren.get_active() )
|
|
self.options_dict['FLmaxChildren' ] = int(self.maxChildren.get_value() )
|
|
self.options_dict['FLincludeImages' ] = int(self.includeImages.get_active() )
|
|
self.options_dict['FLimageOnTheSide' ] = int(self.imageLocation.get_active() )
|
|
self.options_dict['FLincludeDates' ] = int(self.includeDates.get_active() )
|
|
self.options_dict['FLincludePlaces' ] = int(self.includePlaces.get_active() )
|
|
self.options_dict['FLincludeNumChildren'] = int(self.includeNumChildren.get_active())
|
|
self.options_dict['FLincludeResearcher' ] = int(self.includeResearcher.get_active() )
|
|
self.options_dict['FLincludePrivate' ] = int(self.includePrivate.get_active() )
|
|
|
|
# we have a list of usernames & IDs -- save the IDs for next time
|
|
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)
|
|
self.options_dict['FLgidlist' ] = gidlist
|
|
|
|
colour = self.colourMales.get_color()
|
|
colourName = '#%02x%02x%02x' % (
|
|
int(colour.red *256/65536),
|
|
int(colour.green*256/65536),
|
|
int(colour.blue *256/65536))
|
|
self.options_dict['FLcolourMales'] = colourName
|
|
|
|
colour = self.colourFemales.get_color()
|
|
colourName = '#%02x%02x%02x' % (
|
|
int(colour.red *256/65536),
|
|
int(colour.green*256/65536),
|
|
int(colour.blue *256/65536))
|
|
self.options_dict['FLcolourFemales'] = colourName
|
|
|
|
colour = self.colourUnknown.get_color()
|
|
colourName = '#%02x%02x%02x' % (
|
|
int(colour.red *256/65536),
|
|
int(colour.green*256/65536),
|
|
int(colour.blue *256/65536))
|
|
self.options_dict['FLcolourUnknown'] = colourName
|
|
|
|
colour = self.colourFamilies.get_color()
|
|
colourName = '#%02x%02x%02x' % (
|
|
int(colour.red *256/65536),
|
|
int(colour.green*256/65536),
|
|
int(colour.blue *256/65536))
|
|
self.options_dict['FLcolourFamilies'] = colourName
|
|
|
|
surnameColours = ''
|
|
iter = self.familyLinesModel.get_iter_first()
|
|
while (iter):
|
|
surname = self.familyLinesModel.get_value(iter, 0) # .encode('iso-8859-1','xmlcharrefreplace')
|
|
colour = self.familyLinesModel.get_value(iter, 1)
|
|
# tried to use a dictionary, and tried to save it as a tuple,
|
|
# but coulnd't get this to work right -- this is lame, but now
|
|
# the surnames and colours are saved as a plain text string
|
|
surnameColours += surname + ' ' + colour + ' '
|
|
iter = self.familyLinesModel.iter_next(iter)
|
|
self.options_dict['FLsurnameColours'] = surnameColours
|
|
|
|
def toggled(self, togglebutton):
|
|
if not self.followParents.get_active():
|
|
self.limitParents.set_active(False)
|
|
self.removeExtraPeople.set_active(False)
|
|
if not self.followChildren.get_active():
|
|
self.limitChildren.set_active(False)
|
|
|
|
self.imageLocation.set_sensitive( self.includeImages.get_active() )
|
|
self.removeExtraPeople.set_sensitive( self.followParents.get_active() )
|
|
self.limitParents.set_sensitive( self.followParents.get_active() )
|
|
self.limitChildren.set_sensitive( self.followChildren.get_active())
|
|
self.maxParents.set_sensitive( self.limitParents.get_active() )
|
|
self.maxChildren.set_sensitive( self.limitChildren.get_active() )
|
|
|
|
def make_default_style(self,default_style):
|
|
"""Make the default output style for the Web Pages Report."""
|
|
pass
|
|
|
|
#------------------------------------------------------------------------
|
|
#
|
|
# Dialog window used to select a surname
|
|
#
|
|
#------------------------------------------------------------------------
|
|
class LastNameDialog(ManagedWindow.ManagedWindow):
|
|
|
|
def __init__(self, database, uistate, track, surnames, skipList=set()):
|
|
|
|
self.title = _('Select surname')
|
|
ManagedWindow.ManagedWindow.__init__(self, uistate, track, self)
|
|
self.dlg = gtk.Dialog(
|
|
None,
|
|
uistate.window,
|
|
gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT | gtk.DIALOG_NO_SEPARATOR,
|
|
(gtk.STOCK_CANCEL, gtk.RESPONSE_REJECT, gtk.STOCK_OK, gtk.RESPONSE_ACCEPT))
|
|
self.dlg.set_position(gtk.WIN_POS_CENTER_ON_PARENT)
|
|
self.set_window(self.dlg, None, self.title)
|
|
self.window.set_default_size(400,400)
|
|
|
|
# build up a container to display all of the people of interest
|
|
self.model = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_INT)
|
|
self.treeView = gtk.TreeView(self.model)
|
|
col1 = gtk.TreeViewColumn(_('Surname'), gtk.CellRendererText(), text=0)
|
|
col2 = gtk.TreeViewColumn(_('Count'), 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.dlg.vbox.pack_start(self.scrolledWindow, expand=True, fill=True)
|
|
self.scrolledWindow.show_all()
|
|
|
|
if len(surnames) == 0:
|
|
# we could use database.get_surname_list(), but if we do that
|
|
# all we get is a list of names without a count...therefore
|
|
# we'll traverse the entire database ourself and build up a
|
|
# list that we can use
|
|
# for name in database.get_surname_list():
|
|
# self.model.append([name, 0])
|
|
|
|
# build up the list of surnames, keeping track of the count for each name
|
|
# (this can be a lengthy process, so by passing in the dictionary we can
|
|
# be certain we only do this once)
|
|
progress = Utils.ProgressMeter(_('Family Lines'))
|
|
progress.set_pass(_('Finding surnames'), database.get_number_of_people())
|
|
for personHandle in database.get_person_handles(False):
|
|
progress.step()
|
|
person = database.get_person_from_handle(personHandle)
|
|
key = person.get_primary_name().get_surname()
|
|
count = 0
|
|
if key in surnames:
|
|
count = surnames[key]
|
|
surnames[key] = count + 1
|
|
progress.close()
|
|
|
|
# insert the names and count into the model
|
|
for key in surnames:
|
|
if key.encode('iso-8859-1','xmlcharrefreplace') not in skipList:
|
|
self.model.append([key, surnames[key]])
|
|
|
|
# keep the list sorted starting with the most popular last name
|
|
self.model.set_sort_column_id(1, gtk.SORT_DESCENDING)
|
|
|
|
# the "OK" button should be enabled/disabled based on the selection of a row
|
|
self.treeSelection = self.treeView.get_selection()
|
|
self.treeSelection.set_mode(gtk.SELECTION_MULTIPLE)
|
|
self.treeSelection.select_path(0)
|
|
|
|
def run(self):
|
|
response = self.dlg.run()
|
|
surnameSet = set()
|
|
if response == gtk.RESPONSE_ACCEPT:
|
|
(mode, paths) = self.treeSelection.get_selected_rows()
|
|
for path in paths:
|
|
iter = self.model.get_iter(path)
|
|
surname = self.model.get_value(iter, 0)
|
|
surnameSet.add(surname)
|
|
self.dlg.destroy()
|
|
return surnameSet
|
|
|
|
#------------------------------------------------------------------------
|
|
#
|
|
# class ReportDialog is in _ReportDialog.py, which in turn is derived
|
|
# from BaseReportDialog in _BaseReportDialog.py
|
|
#
|
|
# this is where we need to create the dialog window with all of the
|
|
# GUI controls
|
|
#
|
|
#------------------------------------------------------------------------
|
|
class FamilyLinesDialog(ReportDialog):
|
|
|
|
HELP_TOPIC = None
|
|
|
|
def __init__(self, dbstate, uistate, person):
|
|
self.database = dbstate.db
|
|
self.person = person
|
|
name = "familylines"
|
|
translated_name = _("Family Lines")
|
|
self.options = FamilyLinesOptions(name, self) # class which derives from ReportOptions (_ReportOptions.py)
|
|
self.category = CATEGORY_CODE
|
|
ReportDialog.__init__(self, dbstate, uistate, person, self.options, name, translated_name)
|
|
self.style_name = None
|
|
|
|
self.surnames = {} # list of surnames and count
|
|
|
|
while True:
|
|
response = self.window.run()
|
|
if response == gtk.RESPONSE_OK:
|
|
self.make_report()
|
|
break
|
|
elif response != gtk.RESPONSE_HELP:
|
|
break
|
|
self.close()
|
|
|
|
def addPersonClicked(self, obj):
|
|
|
|
# people we already have in our list must be excluded
|
|
# so we don't end up having people listed mutliple times
|
|
skipList = set()
|
|
iter = self.options.model.get_iter_first()
|
|
while (iter):
|
|
gid = self.options.model.get_value(iter, 1) # get the GID stored in column #1
|
|
person = self.database.get_person_from_gramps_id(gid)
|
|
skipList.add(person.get_handle())
|
|
iter = self.options.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.options.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.database.get_family_from_handle(familyHandle)
|
|
spouseHandle = ReportUtils.find_spouse(person, family)
|
|
if spouseHandle:
|
|
if spouseHandle not in skipList:
|
|
spouse = self.database.get_person_from_handle(spouseHandle)
|
|
text = _('Also include %s as a person of interest?') % 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(_('Family Lines'))
|
|
button = prompt.run()
|
|
prompt.destroy()
|
|
if button == gtk.RESPONSE_YES:
|
|
name = _nd.display(spouse)
|
|
gid = spouse.get_gramps_id()
|
|
self.options.model.append([name, gid])
|
|
|
|
|
|
def delPersonClicked(self, obj):
|
|
(path, column) = self.options.treeView.get_cursor()
|
|
if (path):
|
|
iter = self.options.model.get_iter(path)
|
|
self.options.model.remove(iter)
|
|
|
|
|
|
def familyLinesClicked(self, treeview, path, column):
|
|
# get the surname and colour value for this family
|
|
iter = self.options.familyLinesModel.get_iter(path)
|
|
surname = self.options.familyLinesModel.get_value(iter, 0)
|
|
colour = gtk.gdk.color_parse(self.options.familyLinesModel.get_value(iter, 1))
|
|
|
|
colourDialog = gtk.ColorSelectionDialog('Select colour for %s' % surname)
|
|
colourDialog.colorsel.set_current_color(colour)
|
|
response = colourDialog.run()
|
|
|
|
if response == gtk.RESPONSE_OK:
|
|
colour = colourDialog.colorsel.get_current_color()
|
|
colourName = '#%02x%02x%02x' % (
|
|
int(colour.red *256/65536),
|
|
int(colour.green*256/65536),
|
|
int(colour.blue *256/65536))
|
|
self.options.familyLinesModel.set_value(iter, 1, colourName)
|
|
|
|
colourDialog.destroy()
|
|
|
|
|
|
def addSurnameClicked(self, obj):
|
|
skipList = set()
|
|
iter = self.options.familyLinesModel.get_iter_first()
|
|
while (iter):
|
|
surname = self.options.familyLinesModel.get_value(iter, 0)
|
|
skipList.add(surname.encode('iso-8859-1','xmlcharrefreplace'))
|
|
iter = self.options.familyLinesModel.iter_next(iter)
|
|
|
|
ln = LastNameDialog(self.database, self.uistate, self.track, self.surnames, skipList)
|
|
surnameSet = ln.run()
|
|
for surname in surnameSet:
|
|
self.options.familyLinesModel.append([surname, '#ffffff'])
|
|
|
|
|
|
def delSurnameClicked(self, obj):
|
|
(path, column) = self.options.familyLinesTreeView.get_cursor()
|
|
if (path):
|
|
iter = self.options.familyLinesModel.get_iter(path)
|
|
self.options.familyLinesModel.remove(iter)
|
|
|
|
|
|
def setup_style_frame(self):
|
|
"""The style frame is not used in this dialog."""
|
|
pass
|
|
|
|
def parse_style_frame(self):
|
|
"""The style frame is not used in this dialog."""
|
|
pass
|
|
|
|
def get_target_is_directory(self):
|
|
"""This report creates a single file."""
|
|
return None
|
|
|
|
# def get_default_directory(self):
|
|
# """Get the name of the directory to which the target dialog
|
|
# box should default. This value can be set in the preferences
|
|
# panel."""
|
|
# return '.'
|
|
|
|
def make_document(self):
|
|
"""Do Nothing. This document will be created in the
|
|
make_report routine."""
|
|
pass
|
|
|
|
def setup_format_frame(self):
|
|
"""Do nothing, since we don't want a format frame"""
|
|
pass
|
|
|
|
def parse_format_frame(self):
|
|
"""The format frame is not used in this dialog."""
|
|
pass
|
|
|
|
def make_report(self):
|
|
"""Create the object that will produce the .dot output file."""
|
|
try:
|
|
MyReport = FamilyLinesReport(self.database, self.person, self.options)
|
|
MyReport.write_report()
|
|
except Errors.FilterError, msg:
|
|
(m1,m2) = msg.messages()
|
|
ErrorDialog(m1,m2)
|
|
|
|
#------------------------------------------------------------------------
|
|
#
|
|
# 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',
|
|
modes = MODE_GUI,
|
|
status = _("Stable"),
|
|
category = CATEGORY_CODE,
|
|
description =_("Generates family line graphs using GraphViz."),
|
|
author_name = "Stephane Charette",
|
|
author_email = "stephanecharette@gmail.com",
|
|
report_class = FamilyLinesDialog, # class which will create everything needed for the report
|
|
options_class = None,
|
|
translated_name = _("Family Lines Graph"),
|
|
unsupported = True
|
|
)
|
|
|