Move all draw reports into plugins/drawreport.
svn: r11634
This commit is contained in:
540
src/plugins/drawreport/AncestorTree.py
Normal file
540
src/plugins/drawreport/AncestorTree.py
Normal file
@@ -0,0 +1,540 @@
|
||||
#
|
||||
# Gramps - a GTK+/GNOME based genealogy program
|
||||
#
|
||||
# Copyright (C) 2000-2007 Donald N. Allingham
|
||||
# Copyright (C) 2007-2008 Brian G. Matherly
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation; either version 2 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||
#
|
||||
|
||||
# $Id$
|
||||
|
||||
"""Reports/Graphical Reports/Ancestor Tree"""
|
||||
|
||||
#------------------------------------------------------------------------
|
||||
#
|
||||
# python modules
|
||||
#
|
||||
#------------------------------------------------------------------------
|
||||
import math
|
||||
from TransUtils import sgettext as _
|
||||
#------------------------------------------------------------------------
|
||||
#
|
||||
# GRAMPS modules
|
||||
#
|
||||
#------------------------------------------------------------------------
|
||||
import BaseDoc
|
||||
from SubstKeywords import SubstKeywords
|
||||
from gen.plug import PluginManager
|
||||
from gen.plug.menu import BooleanOption, NumberOption, TextOption, PersonOption
|
||||
from ReportBase import Report, ReportUtils, CATEGORY_DRAW, MenuReportOptions
|
||||
from BasicUtils import name_displayer
|
||||
pt2cm = ReportUtils.pt2cm
|
||||
cm2pt = ReportUtils.cm2pt
|
||||
|
||||
#------------------------------------------------------------------------
|
||||
#
|
||||
# Constants
|
||||
#
|
||||
#------------------------------------------------------------------------
|
||||
_BORN = _('short for born|b.')
|
||||
_DIED = _('short for died|d.')
|
||||
|
||||
#------------------------------------------------------------------------
|
||||
#
|
||||
# log2val
|
||||
#
|
||||
#------------------------------------------------------------------------
|
||||
def log2(val):
|
||||
return int(math.log10(val)/math.log10(2))
|
||||
|
||||
#------------------------------------------------------------------------
|
||||
#
|
||||
# Layout class
|
||||
#
|
||||
#------------------------------------------------------------------------
|
||||
class GenChart:
|
||||
|
||||
def __init__(self,generations):
|
||||
self.generations = generations
|
||||
self.size = (2**(generations))
|
||||
self.array = {}
|
||||
self.map = {}
|
||||
self.compress_map = {}
|
||||
|
||||
self.max_x = 0
|
||||
self.ad = (self.size,generations)
|
||||
|
||||
def set(self,index,value):
|
||||
x = log2(index)
|
||||
y = index - (2**x)
|
||||
delta = int((self.size/(2**(x))))
|
||||
new_y = int((delta/2) + (y)*delta)
|
||||
if not new_y in self.array:
|
||||
self.array[new_y] = {}
|
||||
self.array[new_y][x] = (value,index)
|
||||
self.max_x = max(x,self.max_x)
|
||||
self.map[value] = (new_y,x)
|
||||
|
||||
def index_to_xy(self,index):
|
||||
if index:
|
||||
x = log2(index)
|
||||
ty = index - (2**x)
|
||||
delta = int(self.size/(2**x))
|
||||
y = int(delta/2 + ty*delta)
|
||||
else:
|
||||
x = 0
|
||||
y = self.size/2
|
||||
|
||||
if len(self.compress_map) > 0:
|
||||
return (x,self.compress_map[y])
|
||||
else:
|
||||
return (x,y)
|
||||
|
||||
def get(self,index):
|
||||
(x,y) = self.index_to_xy(index)
|
||||
return self.get_xy(x,y)
|
||||
|
||||
def get_xy(self,x,y):
|
||||
value = 0
|
||||
if y in self.array:
|
||||
if x in self.array[y]:
|
||||
value = self.array[y][x]
|
||||
return value
|
||||
|
||||
def set_xy(self,x,y,value):
|
||||
if not y in self.array:
|
||||
self.array[y] = {}
|
||||
self.array[y][x] = value
|
||||
|
||||
def dimensions(self):
|
||||
return (max(self.array.keys())+1,self.max_x+1)
|
||||
|
||||
def compress(self):
|
||||
new_map = {}
|
||||
new_array = {}
|
||||
old_y = 0
|
||||
new_y = 0
|
||||
|
||||
for key in self.array.keys():
|
||||
i = self.array[key]
|
||||
old_y = key
|
||||
if self.not_blank(i.values()):
|
||||
self.compress_map[old_y] = new_y
|
||||
new_array[new_y] = i
|
||||
x = 0
|
||||
for entry in i:
|
||||
new_map[entry] = (new_y,x)
|
||||
x =+ 1
|
||||
new_y += 1
|
||||
self.array = new_array
|
||||
self.map = new_map
|
||||
self.ad = (new_y,self.ad[1])
|
||||
|
||||
def not_blank(self,line):
|
||||
for i in line:
|
||||
if i and isinstance(i, tuple):
|
||||
return 1
|
||||
return 0
|
||||
|
||||
#------------------------------------------------------------------------
|
||||
#
|
||||
# AncestorTree
|
||||
#
|
||||
#------------------------------------------------------------------------
|
||||
class AncestorTree(Report):
|
||||
|
||||
def __init__(self, database, options_class):
|
||||
"""
|
||||
Create AncestorTree object that produces the report.
|
||||
|
||||
The arguments are:
|
||||
|
||||
database - the GRAMPS database instance
|
||||
person - currently selected person
|
||||
options_class - instance of the Options class for this report
|
||||
|
||||
This report needs the following parameters (class variables)
|
||||
that come in the options class.
|
||||
|
||||
gen - Maximum number of generations to include.
|
||||
pagebbg - Whether to include page breaks between generations.
|
||||
dispf - Display format for the output box.
|
||||
singlep - Whether to scale to fit on a single page.
|
||||
indblank - Whether to include blank pages.
|
||||
compress - Whether to compress chart.
|
||||
"""
|
||||
Report.__init__(self, database, options_class)
|
||||
|
||||
menu = options_class.menu
|
||||
self.display = menu.get_option_by_name('dispf').get_value()
|
||||
self.max_generations = menu.get_option_by_name('maxgen').get_value()
|
||||
self.force_fit = menu.get_option_by_name('singlep').get_value()
|
||||
self.incblank = menu.get_option_by_name('incblank').get_value()
|
||||
self.compress = menu.get_option_by_name('compress').get_value()
|
||||
|
||||
pid = menu.get_option_by_name('pid').get_value()
|
||||
center_person = database.get_person_from_gramps_id(pid)
|
||||
|
||||
name = name_displayer.display_formal(center_person)
|
||||
self.title = _("Ancestor Graph for %s") % name
|
||||
|
||||
self.map = {}
|
||||
self.text = {}
|
||||
|
||||
self.box_width = 0
|
||||
self.box_height = 0
|
||||
self.lines = 0
|
||||
self.scale = 1
|
||||
|
||||
self.apply_filter(center_person.get_handle(),1)
|
||||
|
||||
keys = self.map.keys()
|
||||
keys.sort()
|
||||
max_key = log2(keys[-1])
|
||||
|
||||
self.genchart = GenChart(max_key+1)
|
||||
for key in self.map.keys():
|
||||
self.genchart.set(key,self.map[key])
|
||||
self.calc()
|
||||
|
||||
if self.force_fit:
|
||||
self.scale_styles()
|
||||
|
||||
def apply_filter(self,person_handle,index):
|
||||
"""traverse the ancestors recursively until either the end
|
||||
of a line is found, or until we reach the maximum number of
|
||||
generations that we want to deal with"""
|
||||
|
||||
if (not person_handle) or (index >= 2**self.max_generations):
|
||||
return
|
||||
self.map[index] = person_handle
|
||||
|
||||
self.text[index] = []
|
||||
|
||||
style_sheet = self.doc.get_style_sheet()
|
||||
pstyle = style_sheet.get_paragraph_style("AC2-Normal")
|
||||
font = pstyle.get_font()
|
||||
|
||||
em = self.doc.string_width(font,"m")
|
||||
|
||||
subst = SubstKeywords(self.database,person_handle)
|
||||
self.text[index] = subst.replace_and_clean(self.display)
|
||||
|
||||
for line in self.text[index]:
|
||||
this_box_width = self.doc.string_width(font,line)
|
||||
self.box_width = max(self.box_width,this_box_width)
|
||||
|
||||
self.lines = max(self.lines,len(self.text[index]))
|
||||
|
||||
person = self.database.get_person_from_handle(person_handle)
|
||||
family_handle = person.get_main_parents_family_handle()
|
||||
if family_handle:
|
||||
family = self.database.get_family_from_handle(family_handle)
|
||||
self.apply_filter(family.get_father_handle(),index*2)
|
||||
self.apply_filter(family.get_mother_handle(),(index*2)+1)
|
||||
|
||||
def write_report(self):
|
||||
|
||||
(maxy,maxx) = self.genchart.dimensions()
|
||||
maxh = int(self.uh/self.box_height)
|
||||
|
||||
if self.force_fit:
|
||||
self.print_page(0,maxx,0,maxy,0,0)
|
||||
else:
|
||||
starty = 0
|
||||
coly = 0
|
||||
while starty < maxy:
|
||||
startx = 0
|
||||
colx = 0
|
||||
while startx < maxx:
|
||||
stopx = min(maxx,startx+self.generations_per_page)
|
||||
stopy = min(maxy,starty+maxh)
|
||||
self.print_page(startx,stopx,starty,stopy,colx,coly)
|
||||
colx += 1
|
||||
startx += self.generations_per_page
|
||||
coly += 1
|
||||
starty += maxh
|
||||
|
||||
def calc(self):
|
||||
"""
|
||||
calc - calculate the maximum width that a box needs to be. From
|
||||
that and the page dimensions, calculate the proper place to put
|
||||
the elements on a page.
|
||||
"""
|
||||
style_sheet = self.doc.get_style_sheet()
|
||||
|
||||
self.add_lines()
|
||||
if self.compress:
|
||||
self.genchart.compress()
|
||||
|
||||
self.box_pad_pts = 10
|
||||
if self.title and self.force_fit:
|
||||
pstyle = style_sheet.get_paragraph_style("AC2-Title")
|
||||
tfont = pstyle.get_font()
|
||||
self.offset = pt2cm(1.25 * tfont.get_size())
|
||||
|
||||
gstyle = style_sheet.get_draw_style("AC2-box")
|
||||
shadow_height = gstyle.get_shadow_space()
|
||||
else:
|
||||
# Make space for the page number labels at the bottom.
|
||||
p = style_sheet.get_paragraph_style("AC2-Normal")
|
||||
font = p.get_font()
|
||||
lheight = pt2cm(1.2*font.get_size())
|
||||
lwidth = pt2cm(1.1*self.doc.string_width(font,"(00,00)"))
|
||||
self.page_label_x_offset = self.doc.get_usable_width() - lwidth
|
||||
self.page_label_y_offset = self.doc.get_usable_height() - lheight
|
||||
|
||||
self.offset = pt2cm(1.25 * font.get_size())
|
||||
shadow_height = 0
|
||||
self.uh = self.doc.get_usable_height() - self.offset - shadow_height
|
||||
uw = self.doc.get_usable_width() - pt2cm(self.box_pad_pts)
|
||||
|
||||
calc_width = pt2cm(self.box_width + self.box_pad_pts) + 0.2
|
||||
self.box_width = pt2cm(self.box_width)
|
||||
pstyle = style_sheet.get_paragraph_style("AC2-Normal")
|
||||
font = pstyle.get_font()
|
||||
self.box_height = self.lines*pt2cm(1.25*font.get_size())
|
||||
|
||||
if self.force_fit:
|
||||
(maxy,maxx) = self.genchart.dimensions()
|
||||
|
||||
bw = calc_width/(uw/maxx)
|
||||
bh = self.box_height/(self.uh/maxy)
|
||||
|
||||
self.scale = max(bw,bh)
|
||||
self.box_width = self.box_width/self.scale
|
||||
self.box_height = self.box_height/self.scale
|
||||
self.box_pad_pts = self.box_pad_pts/self.scale
|
||||
|
||||
maxh = int(self.uh/self.box_height)
|
||||
maxw = int(uw/calc_width)
|
||||
|
||||
if log2(maxh) < maxw:
|
||||
self.generations_per_page = int(log2(maxh))
|
||||
else:
|
||||
self.generations_per_page = maxw
|
||||
|
||||
# build array of x indices
|
||||
|
||||
self.delta = pt2cm(self.box_pad_pts) + self.box_width + 0.2
|
||||
if not self.force_fit:
|
||||
calc_width = self.box_width + 0.2 + pt2cm(self.box_pad_pts)
|
||||
remain = self.doc.get_usable_width() - ((self.generations_per_page)*calc_width)
|
||||
self.delta += remain/(self.generations_per_page)
|
||||
|
||||
def scale_styles(self):
|
||||
"""
|
||||
Scale the styles for this report. This must be done in the constructor.
|
||||
"""
|
||||
style_sheet = self.doc.get_style_sheet()
|
||||
|
||||
g = style_sheet.get_draw_style("AC2-box")
|
||||
g.set_shadow(g.get_shadow(),g.get_shadow_space()/self.scale)
|
||||
g.set_line_width(g.get_line_width()/self.scale)
|
||||
style_sheet.add_draw_style("AC2-box",g)
|
||||
|
||||
p = style_sheet.get_paragraph_style("AC2-Normal")
|
||||
font = p.get_font()
|
||||
font.set_size(font.get_size()/self.scale)
|
||||
p.set_font(font)
|
||||
style_sheet.add_paragraph_style("AC2-Normal",p)
|
||||
|
||||
self.doc.set_style_sheet(style_sheet)
|
||||
|
||||
def print_page(self,startx,stopx,starty,stopy,colx,coly):
|
||||
|
||||
if not self.incblank:
|
||||
blank = True
|
||||
for y in range(starty,stopy):
|
||||
for x in range(startx,stopx):
|
||||
if self.genchart.get_xy(x,y) != 0:
|
||||
blank = False
|
||||
break
|
||||
if not blank: break
|
||||
if blank: return
|
||||
|
||||
self.doc.start_page()
|
||||
if self.title and self.force_fit:
|
||||
self.doc.center_text('AC2-title',self.title,self.doc.get_usable_width()/2,0)
|
||||
phys_y = 0
|
||||
for y in range(starty,stopy):
|
||||
phys_x = 0
|
||||
for x in range(startx,stopx):
|
||||
value = self.genchart.get_xy(x,y)
|
||||
if value:
|
||||
if isinstance(value, tuple):
|
||||
(person,index) = value
|
||||
text = '\n'.join(self.text[index])
|
||||
self.doc.draw_box("AC2-box",
|
||||
text,
|
||||
phys_x*self.delta,
|
||||
phys_y*self.box_height+self.offset,
|
||||
self.box_width,
|
||||
self.box_height )
|
||||
elif value == 2:
|
||||
self.doc.draw_line("AC2-line",
|
||||
phys_x*self.delta+self.box_width*0.5,
|
||||
phys_y*self.box_height+self.offset,
|
||||
phys_x*self.delta+self.box_width*0.5,
|
||||
(phys_y+1)*self.box_height+self.offset)
|
||||
elif value == 1:
|
||||
x1 = phys_x*self.delta+self.box_width*0.5
|
||||
x2 = (phys_x+1)*self.delta
|
||||
y1 = phys_y*self.box_height+self.offset+self.box_height/2
|
||||
y2 = (phys_y+1)*self.box_height+self.offset
|
||||
self.doc.draw_line("AC2-line",x1,y1,x1,y2)
|
||||
self.doc.draw_line("AC2-line",x1,y1,x2,y1)
|
||||
elif value == 3:
|
||||
x1 = phys_x*self.delta+self.box_width*0.5
|
||||
x2 = (phys_x+1)*self.delta
|
||||
y1 = (phys_y)*self.box_height+self.offset+self.box_height/2
|
||||
y2 = (phys_y)*self.box_height+self.offset
|
||||
self.doc.draw_line("AC2-line",x1,y1,x1,y2)
|
||||
self.doc.draw_line("AC2-line",x1,y1,x2,y1)
|
||||
|
||||
phys_x +=1
|
||||
phys_y += 1
|
||||
|
||||
if not self.force_fit:
|
||||
self.doc.draw_text('AC2-box',
|
||||
'(%d,%d)' % (colx+1,coly+1),
|
||||
self.page_label_x_offset,
|
||||
self.page_label_y_offset)
|
||||
self.doc.end_page()
|
||||
|
||||
def add_lines(self):
|
||||
|
||||
(my,mx) = self.genchart.dimensions()
|
||||
|
||||
for y in range(0,my):
|
||||
for x in range(0,mx):
|
||||
value = self.genchart.get_xy(x,y)
|
||||
if not value:
|
||||
continue
|
||||
if isinstance(value, tuple):
|
||||
(person,index) = value
|
||||
if self.genchart.get(index*2):
|
||||
(px,py) = self.genchart.index_to_xy(index*2)
|
||||
self.genchart.set_xy(x,py,1)
|
||||
for ty in range(py+1,y):
|
||||
self.genchart.set_xy(x,ty,2)
|
||||
if self.genchart.get(index*2+1):
|
||||
(px,py) = self.genchart.index_to_xy(index*2+1)
|
||||
self.genchart.set_xy(px-1,py,3)
|
||||
for ty in range(y+1,py):
|
||||
self.genchart.set_xy(x,ty,2)
|
||||
|
||||
#------------------------------------------------------------------------
|
||||
#
|
||||
# AncestorTreeOptions
|
||||
#
|
||||
#------------------------------------------------------------------------
|
||||
class AncestorTreeOptions(MenuReportOptions):
|
||||
|
||||
"""
|
||||
Defines options and provides handling interface.
|
||||
"""
|
||||
|
||||
def __init__(self, name, dbase):
|
||||
MenuReportOptions.__init__(self, name, dbase)
|
||||
|
||||
def add_menu_options(self, menu):
|
||||
|
||||
category_name = _("Tree Options")
|
||||
|
||||
pid = PersonOption(_("Center Person"))
|
||||
pid.set_help(_("The center person for the tree"))
|
||||
menu.add_option(category_name, "pid", pid)
|
||||
|
||||
max_gen = NumberOption(_("Generations"), 10, 1, 15)
|
||||
max_gen.set_help(_("The number of generations to include in the tree"))
|
||||
menu.add_option(category_name, "maxgen", max_gen)
|
||||
|
||||
disp = TextOption(_("Display Format"),
|
||||
["$n","%s $b" % _BORN,"%s $d" %_DIED] )
|
||||
disp.set_help(_("Display format for the outputbox."))
|
||||
menu.add_option(category_name, "dispf", disp)
|
||||
|
||||
scale = BooleanOption(_('Sc_ale to fit on a single page'), True)
|
||||
scale.set_help(_("Whether to scale to fit on a single page."))
|
||||
menu.add_option(category_name, "singlep", scale)
|
||||
|
||||
blank = BooleanOption(_('Include Blank Pages'), True)
|
||||
blank.set_help(_("Whether to include pages that are blank."))
|
||||
menu.add_option(category_name, "incblank", blank)
|
||||
|
||||
compress = BooleanOption(_('Co_mpress tree'), True)
|
||||
compress.set_help(_("Whether to compress the tree."))
|
||||
menu.add_option(category_name, "compress", compress)
|
||||
|
||||
def make_default_style(self, default_style):
|
||||
"""Make the default output style for the Ancestor Tree."""
|
||||
|
||||
## Paragraph Styles:
|
||||
f = BaseDoc.FontStyle()
|
||||
f.set_size(9)
|
||||
f.set_type_face(BaseDoc.FONT_SANS_SERIF)
|
||||
p = BaseDoc.ParagraphStyle()
|
||||
p.set_font(f)
|
||||
p.set_description(_('The basic style used for the text display.'))
|
||||
default_style.add_paragraph_style("AC2-Normal", p)
|
||||
|
||||
f = BaseDoc.FontStyle()
|
||||
f.set_size(16)
|
||||
f.set_type_face(BaseDoc.FONT_SANS_SERIF)
|
||||
p = BaseDoc.ParagraphStyle()
|
||||
p.set_font(f)
|
||||
p.set_alignment(BaseDoc.PARA_ALIGN_CENTER)
|
||||
p.set_description(_('The basic style used for the title display.'))
|
||||
default_style.add_paragraph_style("AC2-Title", p)
|
||||
|
||||
## Draw styles
|
||||
g = BaseDoc.GraphicsStyle()
|
||||
g.set_paragraph_style("AC2-Normal")
|
||||
g.set_shadow(1, 0.2)
|
||||
g.set_fill_color((255, 255, 255))
|
||||
default_style.add_draw_style("AC2-box", g)
|
||||
|
||||
g = BaseDoc.GraphicsStyle()
|
||||
g.set_paragraph_style("AC2-Title")
|
||||
g.set_color((0, 0, 0))
|
||||
g.set_fill_color((255, 255, 255))
|
||||
g.set_line_width(0)
|
||||
default_style.add_draw_style("AC2-title", g)
|
||||
|
||||
g = BaseDoc.GraphicsStyle()
|
||||
default_style.add_draw_style("AC2-line", g)
|
||||
|
||||
#------------------------------------------------------------------------
|
||||
#
|
||||
#
|
||||
#
|
||||
#------------------------------------------------------------------------
|
||||
pmgr = PluginManager.get_instance()
|
||||
pmgr.register_report(
|
||||
name = 'ancestor_chart',
|
||||
category = CATEGORY_DRAW,
|
||||
report_class = AncestorTree,
|
||||
options_class = AncestorTreeOptions,
|
||||
modes = PluginManager.REPORT_MODE_GUI | \
|
||||
PluginManager.REPORT_MODE_BKI | \
|
||||
PluginManager.REPORT_MODE_CLI,
|
||||
translated_name = _("Ancestor Tree"),
|
||||
status = _("Stable"),
|
||||
author_name = "Donald N. Allingham",
|
||||
author_email = "don@gramps-project.org",
|
||||
description = _("Produces a graphical ancestral tree"),
|
||||
)
|
||||
495
src/plugins/drawreport/DescendTree.py
Normal file
495
src/plugins/drawreport/DescendTree.py
Normal file
@@ -0,0 +1,495 @@
|
||||
#
|
||||
# Gramps - a GTK+/GNOME based genealogy program
|
||||
#
|
||||
# Copyright (C) 2000-2007 Donald N. Allingham
|
||||
# Copyright (C) 2007-2008 Brian G. Matherly
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation; either version 2 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||
#
|
||||
|
||||
# $Id$
|
||||
|
||||
"""Reports/Graphical Reports/Descendant Tree"""
|
||||
|
||||
#------------------------------------------------------------------------
|
||||
#
|
||||
# python modules
|
||||
#
|
||||
#------------------------------------------------------------------------
|
||||
from BasicUtils import name_displayer
|
||||
from gen.plug import PluginManager
|
||||
from gen.plug.menu import TextOption, NumberOption, BooleanOption, PersonOption
|
||||
from ReportBase import Report, MenuReportOptions, ReportUtils, CATEGORY_DRAW
|
||||
from SubstKeywords import SubstKeywords
|
||||
from TransUtils import sgettext as _
|
||||
import BaseDoc
|
||||
|
||||
#------------------------------------------------------------------------
|
||||
#
|
||||
# GRAMPS modules
|
||||
#
|
||||
#------------------------------------------------------------------------
|
||||
pt2cm = ReportUtils.pt2cm
|
||||
cm2pt = ReportUtils.cm2pt
|
||||
|
||||
#------------------------------------------------------------------------
|
||||
#
|
||||
# Constants
|
||||
#
|
||||
#------------------------------------------------------------------------
|
||||
_BORN = _('short for born|b.')
|
||||
_DIED = _('short for died|d.')
|
||||
|
||||
_LINE_HORIZONTAL = 1
|
||||
_LINE_VERTICAL = 2
|
||||
_LINE_ANGLE = 3
|
||||
|
||||
#------------------------------------------------------------------------
|
||||
#
|
||||
# Layout class
|
||||
#
|
||||
#------------------------------------------------------------------------
|
||||
class GenChart:
|
||||
|
||||
def __init__(self,generations):
|
||||
self.generations = generations
|
||||
self.map = {}
|
||||
|
||||
self.array = {}
|
||||
self.max_x = 0
|
||||
self.max_y = 0
|
||||
|
||||
def get_xy(self,x,y):
|
||||
if y not in self.array:
|
||||
return 0
|
||||
return self.array[y].get(x,0)
|
||||
|
||||
def set_xy(self,x,y,value):
|
||||
self.max_x = max(self.max_x,x)
|
||||
self.max_y = max(self.max_y,y)
|
||||
|
||||
if y not in self.array:
|
||||
self.array[y] = {}
|
||||
self.array[y][x] = value
|
||||
|
||||
def dimensions(self):
|
||||
return (self.max_y+1,self.max_x+1)
|
||||
|
||||
def not_blank(self,line):
|
||||
for i in line:
|
||||
if i and isinstance(i, tuple):
|
||||
return 1
|
||||
return 0
|
||||
|
||||
#------------------------------------------------------------------------
|
||||
#
|
||||
# DescendTree
|
||||
#
|
||||
#------------------------------------------------------------------------
|
||||
class DescendTree(Report):
|
||||
|
||||
def __init__(self, database, options_class):
|
||||
"""
|
||||
Create DescendTree object that produces the report.
|
||||
|
||||
The arguments are:
|
||||
|
||||
database - the GRAMPS database instance
|
||||
person - currently selected person
|
||||
options_class - instance of the Options class for this report
|
||||
|
||||
This report needs the following parameters (class variables)
|
||||
that come in the options class.
|
||||
|
||||
dispf - Display format for the output box.
|
||||
singlep - Whether to scale to fit on a single page.
|
||||
maxgen - Maximum number of generations to include.
|
||||
"""
|
||||
Report.__init__(self, database, options_class)
|
||||
|
||||
menu = options_class.menu
|
||||
self.display = menu.get_option_by_name('dispf').get_value()
|
||||
self.max_generations = menu.get_option_by_name('maxgen').get_value()
|
||||
self.force_fit = menu.get_option_by_name('singlep').get_value()
|
||||
self.incblank = menu.get_option_by_name('incblank').get_value()
|
||||
pid = menu.get_option_by_name('pid').get_value()
|
||||
center_person = database.get_person_from_gramps_id(pid)
|
||||
|
||||
name = name_displayer.display_formal(center_person)
|
||||
self.title = _("Descendant Chart for %s") % name
|
||||
|
||||
self.map = {}
|
||||
self.text = {}
|
||||
|
||||
self.box_width = 0
|
||||
self.box_height = 0
|
||||
self.lines = 0
|
||||
self.scale = 1
|
||||
self.box_gap = 0.2
|
||||
|
||||
self.genchart = GenChart(32)
|
||||
|
||||
self.apply_filter(center_person.get_handle(),0,0)
|
||||
|
||||
self.calc()
|
||||
|
||||
if self.force_fit:
|
||||
self.scale_styles()
|
||||
|
||||
def apply_filter(self,person_handle,x,y):
|
||||
"""traverse the ancestors recursively until either the end
|
||||
of a line is found, or until we reach the maximum number of
|
||||
generations that we want to deal with"""
|
||||
|
||||
if x/2 >= self.max_generations:
|
||||
return 0
|
||||
|
||||
self.genchart.set_xy(x,y,person_handle)
|
||||
|
||||
person = self.database.get_person_from_handle(person_handle)
|
||||
|
||||
index = 0
|
||||
|
||||
style_sheet = self.doc.get_style_sheet()
|
||||
pstyle = style_sheet.get_paragraph_style("DC2-Normal")
|
||||
font = pstyle.get_font()
|
||||
|
||||
em = self.doc.string_width(font,"m")
|
||||
|
||||
subst = SubstKeywords(self.database,person_handle)
|
||||
self.text[(x,y)] = subst.replace_and_clean(self.display)
|
||||
for line in self.text[(x,y)]:
|
||||
this_box_width = self.doc.string_width(font,line) + 2*em
|
||||
self.box_width = max(self.box_width,this_box_width)
|
||||
|
||||
self.lines = max(self.lines,len(self.text[(x,y)]))
|
||||
|
||||
new_y = y
|
||||
for family_handle in person.get_family_handle_list():
|
||||
|
||||
family = self.database.get_family_from_handle(family_handle)
|
||||
|
||||
for child_ref in family.get_child_ref_list():
|
||||
sub = self.apply_filter(child_ref.ref, x+2, new_y)
|
||||
index += sub
|
||||
new_y += sub
|
||||
|
||||
return max(1,index)
|
||||
|
||||
def add_lines(self):
|
||||
|
||||
(maxy,maxx) = self.genchart.dimensions()
|
||||
|
||||
for y in range(0,maxy+1):
|
||||
for x in range(0,maxx+1):
|
||||
# skip columns reserved for rows - no data here
|
||||
if x%2:
|
||||
continue
|
||||
|
||||
# if we have a person directly next to a person, that
|
||||
# person must be a child of the first person
|
||||
|
||||
if self.genchart.get_xy(x,y) and self.genchart.get_xy(x+2,y):
|
||||
self.genchart.set_xy(x+1,y,_LINE_HORIZONTAL)
|
||||
else:
|
||||
continue
|
||||
|
||||
# look through the entries below this one. All people in the
|
||||
# next column are descendants until we hit a person in our own
|
||||
# column.
|
||||
|
||||
last = y
|
||||
for newy in range(y+1,maxy+1):
|
||||
if self.genchart.get_xy(x, newy):
|
||||
break
|
||||
|
||||
# if the next position is occupied, we need an
|
||||
# angle, otherwise, we may need a vertical line.
|
||||
if self.genchart.get_xy(x+2, newy):
|
||||
self.genchart.set_xy(x+1, newy,_LINE_ANGLE)
|
||||
for tempy in range(last+1, newy):
|
||||
self.genchart.set_xy(x+1,tempy,_LINE_VERTICAL)
|
||||
last = newy
|
||||
|
||||
def write_report(self):
|
||||
|
||||
(maxy,maxx) = self.genchart.dimensions()
|
||||
maxx = (maxx-1)*2
|
||||
maxh = int((self.uh-0.75)/(self.box_height*1.25))
|
||||
|
||||
if self.force_fit:
|
||||
self.print_page(0,maxx,0,maxy,0,0)
|
||||
else:
|
||||
starty = 0
|
||||
coly = 0
|
||||
while starty < maxy:
|
||||
startx = 0
|
||||
colx = 0
|
||||
while startx < maxx:
|
||||
stopx = min(maxx,startx+self.generations_per_page*2)
|
||||
stopy = min(maxy,starty+maxh)
|
||||
self.print_page(startx,stopx,starty,stopy,colx,coly)
|
||||
colx += 1
|
||||
startx += self.generations_per_page*2
|
||||
coly += 1
|
||||
starty += maxh
|
||||
|
||||
def calc(self):
|
||||
"""
|
||||
calc - calculate the maximum width that a box needs to be. From
|
||||
that and the page dimensions, calculate the proper place to put
|
||||
the elements on a page.
|
||||
"""
|
||||
style_sheet = self.doc.get_style_sheet()
|
||||
|
||||
self.add_lines()
|
||||
|
||||
self.box_pad_pts = 10
|
||||
if self.title and self.force_fit:
|
||||
pstyle = style_sheet.get_paragraph_style("DC2-Title")
|
||||
tfont = pstyle.get_font()
|
||||
self.offset = pt2cm(1.25 * tfont.get_size())
|
||||
|
||||
gstyle = style_sheet.get_draw_style("DC2-box")
|
||||
shadow_height = gstyle.get_shadow_space()
|
||||
else:
|
||||
# Make space for the page number labels at the bottom.
|
||||
p = style_sheet.get_paragraph_style("DC2-Normal")
|
||||
font = p.get_font()
|
||||
lheight = pt2cm(1.2*font.get_size())
|
||||
lwidth = pt2cm(1.1*self.doc.string_width(font,"(00,00)"))
|
||||
self.page_label_x_offset = self.doc.get_usable_width() - lwidth
|
||||
self.page_label_y_offset = self.doc.get_usable_height() - lheight
|
||||
|
||||
self.offset = pt2cm(1.25 * font.get_size())
|
||||
shadow_height = 0
|
||||
self.uh = self.doc.get_usable_height() - self.offset - shadow_height
|
||||
uw = self.doc.get_usable_width() - pt2cm(self.box_pad_pts)
|
||||
|
||||
calc_width = pt2cm(self.box_width + self.box_pad_pts) + self.box_gap
|
||||
self.box_width = pt2cm(self.box_width)
|
||||
pstyle = style_sheet.get_paragraph_style("DC2-Normal")
|
||||
font = pstyle.get_font()
|
||||
self.box_height = self.lines*pt2cm(1.25*font.get_size())
|
||||
|
||||
self.scale = 1
|
||||
|
||||
if self.force_fit:
|
||||
(maxy,maxx) = self.genchart.dimensions()
|
||||
|
||||
bw = (calc_width/(uw/(maxx+1)))
|
||||
bh = (self.box_height*(1.25)+self.box_gap)/(self.uh/maxy)
|
||||
|
||||
self.scale = max(bw/2,bh)
|
||||
self.box_width = self.box_width/self.scale
|
||||
self.box_height = self.box_height/self.scale
|
||||
self.box_pad_pts = self.box_pad_pts/self.scale
|
||||
self.box_gap = self.box_gap/self.scale
|
||||
|
||||
# maxh = int((self.uh)/(self.box_height+self.box_gap))
|
||||
maxw = int(uw/calc_width)
|
||||
|
||||
# build array of x indices
|
||||
|
||||
self.generations_per_page = maxw
|
||||
|
||||
self.delta = pt2cm(self.box_pad_pts) + self.box_width + self.box_gap
|
||||
if not self.force_fit:
|
||||
calc_width = self.box_width + pt2cm(self.box_pad_pts)
|
||||
remain = self.doc.get_usable_width() - ((self.generations_per_page)*calc_width)
|
||||
self.delta += remain/float(self.generations_per_page)
|
||||
|
||||
def scale_styles(self):
|
||||
"""
|
||||
Scale the styles for this report. This must be done in the constructor.
|
||||
"""
|
||||
style_sheet = self.doc.get_style_sheet()
|
||||
|
||||
g = style_sheet.get_draw_style("DC2-box")
|
||||
g.set_shadow(g.get_shadow(),g.get_shadow_space()/self.scale)
|
||||
g.set_line_width(g.get_line_width()/self.scale)
|
||||
style_sheet.add_draw_style("DC2-box",g)
|
||||
|
||||
p = style_sheet.get_paragraph_style("DC2-Normal")
|
||||
font = p.get_font()
|
||||
font.set_size(font.get_size()/self.scale)
|
||||
p.set_font(font)
|
||||
style_sheet.add_paragraph_style("DC2-Normal",p)
|
||||
|
||||
self.doc.set_style_sheet(style_sheet)
|
||||
|
||||
def print_page(self,startx,stopx,starty,stopy,colx,coly):
|
||||
|
||||
if not self.incblank:
|
||||
blank = True
|
||||
for y in range(starty,stopy):
|
||||
for x in range(startx,stopx):
|
||||
if self.genchart.get_xy(x,y) != 0:
|
||||
blank = False
|
||||
break
|
||||
if not blank: break
|
||||
if blank: return
|
||||
|
||||
self.doc.start_page()
|
||||
if self.title and self.force_fit:
|
||||
self.doc.center_text('DC2-title',self.title,self.doc.get_usable_width()/2,0)
|
||||
phys_y = 1
|
||||
bh = self.box_height * 1.25
|
||||
for y in range(starty,stopy):
|
||||
phys_x = 0
|
||||
for x in range(startx,stopx):
|
||||
value = self.genchart.get_xy(x,y)
|
||||
if isinstance(value, basestring):
|
||||
text = '\n'.join(self.text[(x,y)])
|
||||
xbegin = phys_x*self.delta
|
||||
yend = phys_y*bh+self.offset
|
||||
self.doc.draw_box("DC2-box",
|
||||
text,
|
||||
xbegin,
|
||||
yend,
|
||||
self.box_width,
|
||||
self.box_height)
|
||||
elif value == _LINE_HORIZONTAL:
|
||||
xbegin = phys_x*self.delta
|
||||
ystart = (phys_y*bh + self.box_height/2.0) + self.offset
|
||||
xstart = xbegin + self.box_width
|
||||
xstop = (phys_x+1)*self.delta
|
||||
self.doc.draw_line('DC2-line', xstart, ystart, xstop, ystart)
|
||||
elif value == _LINE_VERTICAL:
|
||||
ystart = ((phys_y-1)*bh + self.box_height/2.0) + self.offset
|
||||
ystop = (phys_y*bh + self.box_height/2.0) + self.offset
|
||||
xlast = (phys_x*self.delta) + self.box_width + self.box_gap
|
||||
self.doc.draw_line('DC2-line', xlast, ystart, xlast, ystop)
|
||||
elif value == _LINE_ANGLE:
|
||||
ystart = ((phys_y-1)*bh + self.box_height/2.0) + self.offset
|
||||
ystop = (phys_y*bh + self.box_height/2.0) + self.offset
|
||||
xlast = (phys_x*self.delta) + self.box_width + self.box_gap
|
||||
xnext = (phys_x+1)*self.delta
|
||||
self.doc.draw_line('DC2-line', xlast, ystart, xlast, ystop)
|
||||
self.doc.draw_line('DC2-line', xlast, ystop, xnext, ystop)
|
||||
|
||||
if x%2:
|
||||
phys_x +=1
|
||||
phys_y += 1
|
||||
|
||||
if not self.force_fit:
|
||||
self.doc.draw_text('DC2-box',
|
||||
'(%d,%d)' % (colx+1,coly+1),
|
||||
self.page_label_x_offset,
|
||||
self.page_label_y_offset)
|
||||
self.doc.end_page()
|
||||
|
||||
#------------------------------------------------------------------------
|
||||
#
|
||||
# DescendTreeOptions
|
||||
#
|
||||
#------------------------------------------------------------------------
|
||||
class DescendTreeOptions(MenuReportOptions):
|
||||
|
||||
"""
|
||||
Defines options and provides handling interface.
|
||||
"""
|
||||
|
||||
def __init__(self, name, dbase):
|
||||
MenuReportOptions.__init__(self, name, dbase)
|
||||
|
||||
def add_menu_options(self, menu):
|
||||
"""
|
||||
Add options to the menu for the descendant report.
|
||||
"""
|
||||
category_name = _("Tree Options")
|
||||
|
||||
pid = PersonOption(_("Center Person"))
|
||||
pid.set_help(_("The center person for the tree"))
|
||||
menu.add_option(category_name, "pid", pid)
|
||||
|
||||
max_gen = NumberOption(_("Generations"), 10, 1, 50)
|
||||
max_gen.set_help(_("The number of generations to include in the tree"))
|
||||
menu.add_option(category_name, "maxgen", max_gen)
|
||||
|
||||
disp = TextOption( _("Display Format"),
|
||||
["$n","%s $b" % _BORN,"%s $d" %_DIED] )
|
||||
disp.set_help(_("Display format for the outputbox."))
|
||||
menu.add_option(category_name, "dispf", disp)
|
||||
|
||||
scale = BooleanOption(_('Sc_ale to fit on a single page'), True)
|
||||
scale.set_help(_("Whether to scale to fit on a single page."))
|
||||
menu.add_option(category_name, "singlep", scale)
|
||||
|
||||
blank = BooleanOption(_('Include Blank Pages'), True)
|
||||
blank.set_help(_("Whether to include pages that are blank."))
|
||||
menu.add_option(category_name, "incblank", blank)
|
||||
|
||||
compress = BooleanOption(_('Co_mpress tree'),True)
|
||||
compress.set_help(_("Whether to compress tree."))
|
||||
menu.add_option(category_name, "compress", compress)
|
||||
|
||||
def make_default_style(self,default_style):
|
||||
"""Make the default output style for the Ancestor Tree."""
|
||||
## Paragraph Styles:
|
||||
f = BaseDoc.FontStyle()
|
||||
f.set_size(9)
|
||||
f.set_type_face(BaseDoc.FONT_SANS_SERIF)
|
||||
p = BaseDoc.ParagraphStyle()
|
||||
p.set_font(f)
|
||||
p.set_description(_('The basic style used for the text display.'))
|
||||
default_style.add_paragraph_style("DC2-Normal",p)
|
||||
|
||||
f = BaseDoc.FontStyle()
|
||||
f.set_size(16)
|
||||
f.set_type_face(BaseDoc.FONT_SANS_SERIF)
|
||||
p = BaseDoc.ParagraphStyle()
|
||||
p.set_font(f)
|
||||
p.set_alignment(BaseDoc.PARA_ALIGN_CENTER)
|
||||
p.set_description(_('The basic style used for the title display.'))
|
||||
default_style.add_paragraph_style("DC2-Title",p)
|
||||
|
||||
## Draw styles
|
||||
g = BaseDoc.GraphicsStyle()
|
||||
g.set_paragraph_style("DC2-Normal")
|
||||
g.set_shadow(1,0.2)
|
||||
g.set_fill_color((255,255,255))
|
||||
default_style.add_draw_style("DC2-box",g)
|
||||
|
||||
g = BaseDoc.GraphicsStyle()
|
||||
g.set_paragraph_style("DC2-Title")
|
||||
g.set_color((0,0,0))
|
||||
g.set_fill_color((255,255,255))
|
||||
g.set_line_width(0)
|
||||
default_style.add_draw_style("DC2-title",g)
|
||||
|
||||
g = BaseDoc.GraphicsStyle()
|
||||
default_style.add_draw_style("DC2-line",g)
|
||||
|
||||
#------------------------------------------------------------------------
|
||||
#
|
||||
#
|
||||
#
|
||||
#------------------------------------------------------------------------
|
||||
pmgr = PluginManager.get_instance()
|
||||
pmgr.register_report(
|
||||
name = 'descend_chart',
|
||||
category = CATEGORY_DRAW,
|
||||
report_class = DescendTree,
|
||||
options_class = DescendTreeOptions,
|
||||
modes = PluginManager.REPORT_MODE_GUI | \
|
||||
PluginManager.REPORT_MODE_BKI | \
|
||||
PluginManager.REPORT_MODE_CLI,
|
||||
translated_name = _("Descendant Tree"),
|
||||
status = _("Stable"),
|
||||
author_name = "Donald N. Allingham",
|
||||
author_email = "don@gramps-project.org",
|
||||
description = _("Produces a graphical descendant tree"),
|
||||
)
|
||||
449
src/plugins/drawreport/FanChart.py
Normal file
449
src/plugins/drawreport/FanChart.py
Normal file
@@ -0,0 +1,449 @@
|
||||
#
|
||||
# Gramps - a GTK+/GNOME based genealogy program
|
||||
#
|
||||
# Copyright (C) 2003-2006 Donald N. Allingham
|
||||
# Copyright (C) 2007-2008 Brian G. Matherly
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation; either version 2 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||
#
|
||||
|
||||
# $Id$
|
||||
|
||||
#------------------------------------------------------------------------
|
||||
#
|
||||
# python modules
|
||||
#
|
||||
#------------------------------------------------------------------------
|
||||
from gettext import gettext as _
|
||||
|
||||
#------------------------------------------------------------------------
|
||||
#
|
||||
# gramps modules
|
||||
#
|
||||
#------------------------------------------------------------------------
|
||||
import BaseDoc
|
||||
from gen.plug import PluginManager
|
||||
from gen.plug.menu import EnumeratedListOption, NumberOption, PersonOption
|
||||
from ReportBase import Report, ReportUtils, MenuReportOptions, CATEGORY_DRAW
|
||||
from SubstKeywords import SubstKeywords
|
||||
|
||||
#------------------------------------------------------------------------
|
||||
#
|
||||
# private constants
|
||||
#
|
||||
#------------------------------------------------------------------------
|
||||
FULL_CIRCLE = 0
|
||||
HALF_CIRCLE = 1
|
||||
QUAR_CIRCLE = 2
|
||||
|
||||
BACKGROUND_WHITE = 0
|
||||
BACKGROUND_GEN = 1
|
||||
|
||||
RADIAL_UPRIGHT = 0
|
||||
RADIAL_ROUNDABOUT = 1
|
||||
|
||||
pt2cm = ReportUtils.pt2cm
|
||||
|
||||
#------------------------------------------------------------------------
|
||||
#
|
||||
# FanChart
|
||||
#
|
||||
#------------------------------------------------------------------------
|
||||
class FanChart(Report):
|
||||
|
||||
def __init__(self, database, options_class):
|
||||
"""
|
||||
Create the FanChart object that produces the report.
|
||||
|
||||
The arguments are:
|
||||
|
||||
database - the GRAMPS database instance
|
||||
person - currently selected person
|
||||
options_class - instance of the Options class for this report
|
||||
|
||||
This report needs the following parameters (class variables)
|
||||
that come in the options class.
|
||||
|
||||
maxgen - Maximum number of generations to include.
|
||||
circle - Draw a full circle, half circle, or quarter circle.
|
||||
background - Background color is generation dependent or white.
|
||||
radial - Print radial texts roundabout or as upright as possible.
|
||||
"""
|
||||
|
||||
menu = options_class.menu
|
||||
self.max_generations = menu.get_option_by_name('maxgen').get_value()
|
||||
self.circle = menu.get_option_by_name('circle').get_value()
|
||||
self.background = menu.get_option_by_name('background').get_value()
|
||||
self.radial = menu.get_option_by_name('radial').get_value()
|
||||
pid = menu.get_option_by_name('pid').get_value()
|
||||
self.center_person = database.get_person_from_gramps_id(pid)
|
||||
|
||||
self.background_style = []
|
||||
self.text_style = []
|
||||
for i in range (0, self.max_generations):
|
||||
if self.background == BACKGROUND_WHITE:
|
||||
background_style_name = 'background_style_white'
|
||||
else:
|
||||
background_style_name = 'background_style' + '%d' % i
|
||||
self.background_style.append(background_style_name)
|
||||
text_style_name = 'text_style' + '%d' % i
|
||||
self.text_style.append(text_style_name)
|
||||
|
||||
Report.__init__(self, database, options_class)
|
||||
|
||||
self.height = 0
|
||||
self.lines = 0
|
||||
self.display = "%n"
|
||||
self.map = [None] * 2**self.max_generations
|
||||
self.text = {}
|
||||
self.box_width = 0
|
||||
|
||||
def apply_filter(self,person_handle,index):
|
||||
"""traverse the ancestors recursively until either the end
|
||||
of a line is found, or until we reach the maximum number of
|
||||
generations that we want to deal with"""
|
||||
|
||||
if (not person_handle) or (index >= 2**self.max_generations):
|
||||
return
|
||||
self.map[index-1] = person_handle
|
||||
|
||||
self.text[index-1] = []
|
||||
|
||||
subst = SubstKeywords(self.database,person_handle)
|
||||
|
||||
for line in self.display:
|
||||
self.text[index-1].append(subst.replace(line))
|
||||
|
||||
style_sheet = self.doc.get_style_sheet()
|
||||
self.font = style_sheet.get_paragraph_style('text_style').get_font()
|
||||
for line in self.text[index-1]:
|
||||
self.box_width = max(self.box_width,self.doc.string_width(self.font,line))
|
||||
|
||||
self.lines = max(self.lines,len(self.text[index-1]))
|
||||
|
||||
person = self.database.get_person_from_handle(person_handle)
|
||||
family_handle = person.get_main_parents_family_handle()
|
||||
if family_handle:
|
||||
family = self.database.get_family_from_handle(family_handle)
|
||||
self.apply_filter(family.get_father_handle(),index*2)
|
||||
self.apply_filter(family.get_mother_handle(),(index*2)+1)
|
||||
|
||||
|
||||
def write_report(self):
|
||||
|
||||
self.doc.start_page()
|
||||
|
||||
self.apply_filter(self.center_person.get_handle(),1)
|
||||
n = self.center_person.get_primary_name().get_regular_name()
|
||||
|
||||
if self.circle == FULL_CIRCLE:
|
||||
max_angle = 360.0
|
||||
start_angle = 90
|
||||
max_circular = 5
|
||||
x = self.doc.get_usable_width() / 2.0
|
||||
y = self.doc.get_usable_height() / 2.0
|
||||
min_xy = min (x, y)
|
||||
|
||||
elif self.circle == HALF_CIRCLE:
|
||||
max_angle = 180.0
|
||||
start_angle = 180
|
||||
max_circular = 3
|
||||
x = (self.doc.get_usable_width()/2.0)
|
||||
y = self.doc.get_usable_height()
|
||||
min_xy = min (x, y)
|
||||
|
||||
else: # quarter circle
|
||||
max_angle = 90.0
|
||||
start_angle = 270
|
||||
max_circular = 2
|
||||
x = 0
|
||||
y = self.doc.get_usable_height()
|
||||
min_xy = min (self.doc.get_usable_width(), y)
|
||||
|
||||
if self.circle == FULL_CIRCLE or self.circle == QUAR_CIRCLE:
|
||||
# adjust only if full circle or 1/4 circle in landscape mode
|
||||
if self.doc.get_usable_height() <= self.doc.get_usable_width():
|
||||
# Should be in Landscape now
|
||||
style_sheet = self.doc.get_style_sheet()
|
||||
fontsize = pt2cm(style_sheet.get_paragraph_style('FC-Title').get_font().get_size())
|
||||
# y is vertical distance to center of circle, move center down 1 fontsize
|
||||
y = y + fontsize
|
||||
# min_XY is the diamter of the circle, subtract two fontsize
|
||||
# so we dont draw outside bottom of the paper
|
||||
min_xy = min(min_xy,y-2*fontsize)
|
||||
if self.max_generations > max_circular:
|
||||
block_size = min_xy / (self.max_generations * 2 - max_circular)
|
||||
else:
|
||||
block_size = min_xy / self.max_generations
|
||||
text = _("%(generations)d Generation Fan Chart for %(person)s" ) % \
|
||||
{ 'generations' : self.max_generations, 'person' : n }
|
||||
self.doc.center_text ('t', text, self.doc.get_usable_width() / 2, 0)
|
||||
|
||||
for generation in range (0, min (max_circular, self.max_generations)):
|
||||
self.draw_circular (x, y, start_angle, max_angle, block_size, generation)
|
||||
for generation in range (max_circular, self.max_generations):
|
||||
self.draw_radial (x, y, start_angle, max_angle, block_size, generation)
|
||||
|
||||
self.doc.end_page()
|
||||
|
||||
|
||||
def get_info(self,person_handle,generation):
|
||||
person = self.database.get_person_from_handle(person_handle)
|
||||
pn = person.get_primary_name()
|
||||
|
||||
birth_ref = person.get_birth_ref()
|
||||
if birth_ref:
|
||||
birth = self.database.get_event_from_handle(birth_ref.ref)
|
||||
b = birth.get_date_object().get_year()
|
||||
if b == 0:
|
||||
b = ""
|
||||
else:
|
||||
b = ""
|
||||
|
||||
death_ref = person.get_death_ref()
|
||||
if death_ref:
|
||||
death = self.database.get_event_from_handle(death_ref.ref)
|
||||
d = death.get_date_object().get_year()
|
||||
if d == 0:
|
||||
d = ""
|
||||
else:
|
||||
d = ""
|
||||
|
||||
if b and d:
|
||||
val = "%s - %s" % (str(b),str(d))
|
||||
elif b:
|
||||
val = "* %s" % (str(b))
|
||||
elif d:
|
||||
val = "+ %s" % (str(d))
|
||||
else:
|
||||
val = ""
|
||||
|
||||
if generation == 7:
|
||||
if (pn.get_first_name() != "") and (pn.get_surname() != ""):
|
||||
name = pn.get_first_name() + " " + pn.get_surname()
|
||||
else:
|
||||
name = pn.get_first_name() + pn.get_surname()
|
||||
|
||||
if self.circle == FULL_CIRCLE:
|
||||
return [ name, val ]
|
||||
elif self.circle == HALF_CIRCLE:
|
||||
return [ name, val ]
|
||||
else:
|
||||
if (name != "") and (val != ""):
|
||||
string = name + ", " + val
|
||||
else:
|
||||
string = name + val
|
||||
return [string]
|
||||
elif generation == 6:
|
||||
if self.circle == FULL_CIRCLE:
|
||||
return [ pn.get_first_name(), pn.get_surname(), val ]
|
||||
elif self.circle == HALF_CIRCLE:
|
||||
return [ pn.get_first_name(), pn.get_surname(), val ]
|
||||
else:
|
||||
if (pn.get_first_name() != "") and (pn.get_surname() != ""):
|
||||
name = pn.get_first_name() + " " + pn.get_surname()
|
||||
else:
|
||||
name = pn.get_first_name() + pn.get_surname()
|
||||
return [ name, val ]
|
||||
else:
|
||||
return [ pn.get_first_name(), pn.get_surname(), val ]
|
||||
|
||||
|
||||
def draw_circular(self, x, y, start_angle, max_angle, size, generation):
|
||||
segments = 2**generation
|
||||
delta = max_angle / segments
|
||||
end_angle = start_angle
|
||||
text_angle = start_angle - 270 + (delta / 2.0)
|
||||
rad1 = size * generation
|
||||
rad2 = size * (generation + 1)
|
||||
background_style = self.background_style[generation]
|
||||
text_style = self.text_style[generation]
|
||||
|
||||
for index in range(segments - 1, 2*segments - 1):
|
||||
start_angle = end_angle
|
||||
end_angle = start_angle + delta
|
||||
(xc,yc) = ReportUtils.draw_wedge(self.doc,background_style, x, y, rad2,
|
||||
start_angle, end_angle, rad1)
|
||||
if self.map[index]:
|
||||
if (generation == 0) and self.circle == FULL_CIRCLE:
|
||||
yc = y
|
||||
self.doc.rotate_text(text_style,
|
||||
self.get_info(self.map[index],
|
||||
generation),
|
||||
xc, yc, text_angle)
|
||||
text_angle += delta
|
||||
|
||||
|
||||
def draw_radial(self, x, y, start_angle, max_angle, size, generation):
|
||||
segments = 2**generation
|
||||
delta = max_angle / segments
|
||||
end_angle = start_angle
|
||||
text_angle = start_angle - delta / 2.0
|
||||
background_style = self.background_style[generation]
|
||||
text_style = self.text_style[generation]
|
||||
if self.circle == FULL_CIRCLE:
|
||||
rad1 = size * ((generation * 2) - 5)
|
||||
rad2 = size * ((generation * 2) - 3)
|
||||
elif self.circle == HALF_CIRCLE:
|
||||
rad1 = size * ((generation * 2) - 3)
|
||||
rad2 = size * ((generation * 2) - 1)
|
||||
else: # quarter circle
|
||||
rad1 = size * ((generation * 2) - 2)
|
||||
rad2 = size * (generation * 2)
|
||||
|
||||
for index in range(segments - 1, 2*segments - 1):
|
||||
start_angle = end_angle
|
||||
end_angle = start_angle + delta
|
||||
(xc,yc) = ReportUtils.draw_wedge(self.doc,background_style, x, y, rad2,
|
||||
start_angle, end_angle, rad1)
|
||||
text_angle += delta
|
||||
if self.map[index]:
|
||||
if self.radial == RADIAL_UPRIGHT and (start_angle >= 90) and (start_angle < 270):
|
||||
self.doc.rotate_text(text_style,
|
||||
self.get_info(self.map[index],
|
||||
generation),
|
||||
xc, yc, text_angle + 180)
|
||||
else:
|
||||
self.doc.rotate_text(text_style,
|
||||
self.get_info(self.map[index],
|
||||
generation),
|
||||
xc, yc, text_angle)
|
||||
|
||||
#------------------------------------------------------------------------
|
||||
#
|
||||
#
|
||||
#
|
||||
#------------------------------------------------------------------------
|
||||
class FanChartOptions(MenuReportOptions):
|
||||
|
||||
def __init__(self, name, dbase):
|
||||
self.MAX_GENERATIONS = 8
|
||||
|
||||
MenuReportOptions.__init__(self, name, dbase)
|
||||
|
||||
def add_menu_options(self, menu):
|
||||
"""
|
||||
Add options to the menu for the fan chart.
|
||||
"""
|
||||
category_name = _("Report Options")
|
||||
|
||||
pid = PersonOption(_("Center Person"))
|
||||
pid.set_help(_("The center person for the report"))
|
||||
menu.add_option(category_name, "pid", pid)
|
||||
|
||||
max_gen = NumberOption(_("Generations"),5,1,self.MAX_GENERATIONS)
|
||||
max_gen.set_help(_("The number of generations to include in the report"))
|
||||
menu.add_option(category_name,"maxgen",max_gen)
|
||||
|
||||
circle = EnumeratedListOption(_('Type of graph'),HALF_CIRCLE)
|
||||
circle.add_item(FULL_CIRCLE,_('full circle'))
|
||||
circle.add_item(HALF_CIRCLE,_('half circle'))
|
||||
circle.add_item(QUAR_CIRCLE,_('quarter circle'))
|
||||
circle.set_help( _("The form of the graph: full circle, half circle,"
|
||||
" or quarter circle."))
|
||||
menu.add_option(category_name,"circle",circle)
|
||||
|
||||
background = EnumeratedListOption(_('Background color'),BACKGROUND_GEN)
|
||||
background.add_item(BACKGROUND_WHITE,_('white'))
|
||||
background.add_item(BACKGROUND_GEN,_('generation dependent'))
|
||||
background.set_help(_("Background color is either white or generation"
|
||||
" dependent"))
|
||||
menu.add_option(category_name,"background",background)
|
||||
|
||||
radial = EnumeratedListOption( _('Orientation of radial texts'),
|
||||
RADIAL_UPRIGHT )
|
||||
radial.add_item(RADIAL_UPRIGHT,_('upright'))
|
||||
radial.add_item(RADIAL_ROUNDABOUT,_('roundabout'))
|
||||
radial.set_help(_("Print radial texts upright or roundabout"))
|
||||
menu.add_option(category_name,"radial",radial)
|
||||
|
||||
def make_default_style(self,default_style):
|
||||
"""Make the default output style for the Fan Chart report."""
|
||||
BACKGROUND_COLORS = [
|
||||
(255, 63, 0),
|
||||
(255,175, 15),
|
||||
(255,223, 87),
|
||||
(255,255,111),
|
||||
(159,255,159),
|
||||
(111,215,255),
|
||||
( 79,151,255),
|
||||
(231, 23,255)
|
||||
]
|
||||
|
||||
#Paragraph Styles
|
||||
f = BaseDoc.FontStyle()
|
||||
f.set_size(20)
|
||||
f.set_bold(1)
|
||||
f.set_type_face(BaseDoc.FONT_SANS_SERIF)
|
||||
p = BaseDoc.ParagraphStyle()
|
||||
p.set_font(f)
|
||||
p.set_alignment(BaseDoc.PARA_ALIGN_CENTER)
|
||||
p.set_description(_('The style used for the title.'))
|
||||
default_style.add_paragraph_style("FC-Title",p)
|
||||
|
||||
f = BaseDoc.FontStyle()
|
||||
f.set_size(9)
|
||||
f.set_type_face(BaseDoc.FONT_SANS_SERIF)
|
||||
p = BaseDoc.ParagraphStyle()
|
||||
p.set_font(f)
|
||||
p.set_alignment(BaseDoc.PARA_ALIGN_CENTER)
|
||||
p.set_description(_('The basic style used for the text display.'))
|
||||
default_style.add_paragraph_style("text_style", p)
|
||||
|
||||
# GraphicsStyles
|
||||
g = BaseDoc.GraphicsStyle()
|
||||
g.set_paragraph_style('FC-Title')
|
||||
g.set_line_width(0)
|
||||
default_style.add_draw_style("t",g)
|
||||
|
||||
for i in range (0, self.MAX_GENERATIONS):
|
||||
g = BaseDoc.GraphicsStyle()
|
||||
g.set_fill_color(BACKGROUND_COLORS[i])
|
||||
g.set_paragraph_style('FC-Normal')
|
||||
background_style_name = 'background_style' + '%d' % i
|
||||
default_style.add_draw_style(background_style_name,g)
|
||||
|
||||
g = BaseDoc.GraphicsStyle()
|
||||
g.set_fill_color(BACKGROUND_COLORS[i])
|
||||
g.set_paragraph_style('text_style')
|
||||
g.set_line_width(0)
|
||||
text_style_name = 'text_style' + '%d' % i
|
||||
default_style.add_draw_style(text_style_name,g)
|
||||
|
||||
g = BaseDoc.GraphicsStyle()
|
||||
g.set_fill_color((255,255,255))
|
||||
g.set_paragraph_style('FC-Normal')
|
||||
default_style.add_draw_style('background_style_white',g)
|
||||
|
||||
#------------------------------------------------------------------------
|
||||
#
|
||||
#
|
||||
#
|
||||
#------------------------------------------------------------------------
|
||||
pmgr = PluginManager.get_instance()
|
||||
pmgr.register_report(
|
||||
name = 'fan_chart',
|
||||
category = CATEGORY_DRAW,
|
||||
report_class = FanChart,
|
||||
options_class = FanChartOptions,
|
||||
modes = PluginManager.REPORT_MODE_GUI | \
|
||||
PluginManager.REPORT_MODE_BKI | \
|
||||
PluginManager.REPORT_MODE_CLI,
|
||||
translated_name = _("Fan Chart"),
|
||||
status = _("Stable"),
|
||||
author_name = "Donald N. Allingham",
|
||||
author_email = "don@gramps-project.org",
|
||||
description = _("Produces fan charts")
|
||||
)
|
||||
25
src/plugins/drawreport/Makefile.am
Normal file
25
src/plugins/drawreport/Makefile.am
Normal file
@@ -0,0 +1,25 @@
|
||||
# This is the src/plugins/rel level Makefile for Gramps
|
||||
# We could use GNU make's ':=' syntax for nice wildcard use,
|
||||
# but that is not necessarily portable.
|
||||
# If not using GNU make, then list all .py files individually
|
||||
|
||||
pkgdatadir = $(datadir)/@PACKAGE@/plugins/drawreport
|
||||
|
||||
pkgdata_PYTHON = \
|
||||
AncestorTree.py \
|
||||
DescendTree.py \
|
||||
FanChart.py \
|
||||
StatisticsChart.py \
|
||||
TimeLine.py
|
||||
|
||||
pkgpyexecdir = @pkgpyexecdir@/plugins/drawreport
|
||||
pkgpythondir = @pkgpythondir@/plugins/drawreport
|
||||
|
||||
# Clean up all the byte-compiled files
|
||||
MOSTLYCLEANFILES = *pyc *pyo
|
||||
|
||||
GRAMPS_PY_MODPATH = "../../"
|
||||
|
||||
pycheck:
|
||||
(export PYTHONPATH=$(GRAMPS_PY_MODPATH); \
|
||||
pychecker $(pkgdata_PYTHON));
|
||||
909
src/plugins/drawreport/StatisticsChart.py
Normal file
909
src/plugins/drawreport/StatisticsChart.py
Normal file
@@ -0,0 +1,909 @@
|
||||
#
|
||||
# Gramps - a GTK+/GNOME based genealogy program
|
||||
#
|
||||
# Copyright (C) 2003-2006 Donald N. Allingham
|
||||
# Copyright (C) 2004-2005 Eero Tamminen
|
||||
# Copyright (C) 2007-2008 Brian G. Matherly
|
||||
# Copyright (C) 2008 Peter Landgren
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation; either version 2 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||
#
|
||||
# To see things still missing, search for "TODO"...
|
||||
#
|
||||
# $Id$
|
||||
|
||||
"""Reports/Graphical Reports/Statistics Report"""
|
||||
|
||||
#------------------------------------------------------------------------
|
||||
#
|
||||
# python modules
|
||||
#
|
||||
#------------------------------------------------------------------------
|
||||
import time
|
||||
from TransUtils import sgettext as _
|
||||
|
||||
#------------------------------------------------------------------------
|
||||
#
|
||||
# GRAMPS modules
|
||||
#
|
||||
#------------------------------------------------------------------------
|
||||
|
||||
# Person and relation types
|
||||
from gen.lib import Person, FamilyRelType, EventType
|
||||
# gender and report type names
|
||||
import BaseDoc
|
||||
from gen.plug import PluginManager
|
||||
from gen.plug.menu import BooleanOption, NumberOption, EnumeratedListOption, \
|
||||
FilterOption, PersonOption
|
||||
from ReportBase import Report, ReportUtils, MenuReportOptions, CATEGORY_DRAW
|
||||
import DateHandler
|
||||
from Utils import ProgressMeter
|
||||
|
||||
#------------------------------------------------------------------------
|
||||
#
|
||||
# Global options and their names
|
||||
#
|
||||
#------------------------------------------------------------------------
|
||||
|
||||
class _options:
|
||||
# sort type identifiers
|
||||
SORT_VALUE = 0
|
||||
SORT_KEY = 1
|
||||
|
||||
sorts = [
|
||||
(SORT_VALUE, "Item count", _("Item count")),
|
||||
(SORT_KEY, "Item name", _("Item name"))
|
||||
]
|
||||
genders = [
|
||||
(Person.UNKNOWN, "Both", _("Both")),
|
||||
(Person.MALE, "Men", _("Men")),
|
||||
(Person.FEMALE, "Women", _("Women"))
|
||||
]
|
||||
|
||||
|
||||
#------------------------------------------------------------------------
|
||||
#
|
||||
# Data extraction methods from the database
|
||||
#
|
||||
#------------------------------------------------------------------------
|
||||
class Extract:
|
||||
|
||||
def __init__(self):
|
||||
"""Methods for extracting statistical data from the database"""
|
||||
# key, non-localized name, localized name, type method, data method
|
||||
self.extractors = {
|
||||
'data_title': ("Title", _("person|Title"),
|
||||
self.get_person, self.get_title),
|
||||
'data_sname': ("Surname", _("Surname"),
|
||||
self.get_person, self.get_surname),
|
||||
'data_fname': ("Forename", _("Forename"),
|
||||
self.get_person, self.get_forename),
|
||||
'data_gender': ("Gender", _("Gender"),
|
||||
self.get_person, self.get_gender),
|
||||
'data_byear': ("Birth year", _("Birth year"),
|
||||
self.get_birth, self.get_year),
|
||||
'data_dyear': ("Death year", _("Death year"),
|
||||
self.get_death, self.get_year),
|
||||
'data_bmonth': ("Birth month", _("Birth month"),
|
||||
self.get_birth, self.get_month),
|
||||
'data_dmonth': ("Death month", _("Death month"),
|
||||
self.get_death, self.get_month),
|
||||
'data_bplace': ("Birth place", _("Birth place"),
|
||||
self.get_birth, self.get_place),
|
||||
'data_dplace': ("Death place", _("Death place"),
|
||||
self.get_death, self.get_place),
|
||||
'data_mplace': ("Marriage place", _("Marriage place"),
|
||||
self.get_marriage_handles, self.get_places),
|
||||
'data_mcount': ("Number of relationships", _("Number of relationships"),
|
||||
self.get_family_handles, self.get_handle_count),
|
||||
'data_fchild': ("Age when first child born", _("Age when first child born"),
|
||||
self.get_child_handles, self.get_first_child_age),
|
||||
'data_lchild': ("Age when last child born", _("Age when last child born"),
|
||||
self.get_child_handles, self.get_last_child_age),
|
||||
'data_ccount': ("Number of children", _("Number of children"),
|
||||
self.get_child_handles, self.get_handle_count),
|
||||
'data_mage': ("Age at marriage", _("Age at marriage"),
|
||||
self.get_marriage_handles, self.get_event_ages),
|
||||
'data_dage': ("Age at death", _("Age at death"),
|
||||
self.get_person, self.get_death_age),
|
||||
'data_age': ("Age", _("Age"),
|
||||
self.get_person, self.get_person_age),
|
||||
'data_etypes': ("Event type", _("Event type"),
|
||||
self.get_event_handles, self.get_event_type)
|
||||
}
|
||||
|
||||
# ----------------- data extraction methods --------------------
|
||||
# take an object and return a list of strings
|
||||
|
||||
def get_title(self, person):
|
||||
"return title for given person"
|
||||
# TODO: return all titles, not just primary ones...
|
||||
title = person.get_primary_name().get_title()
|
||||
if title:
|
||||
return [title]
|
||||
else:
|
||||
return [_("(Preferred) title missing")]
|
||||
|
||||
def get_forename(self, person):
|
||||
"return forenames for given person"
|
||||
# TODO: return all forenames, not just primary ones...
|
||||
firstnames = person.get_primary_name().get_first_name().strip()
|
||||
if firstnames:
|
||||
return firstnames.split()
|
||||
else:
|
||||
return [_("(Preferred) forename missing")]
|
||||
|
||||
def get_surname(self, person):
|
||||
"return surnames for given person"
|
||||
# TODO: return all surnames, not just primary ones...
|
||||
surnames = person.get_primary_name().get_surname().strip()
|
||||
if surnames:
|
||||
return surnames.split()
|
||||
else:
|
||||
return [_("(Preferred) surname missing")]
|
||||
|
||||
def get_gender(self, person):
|
||||
"return gender for given person"
|
||||
# TODO: why there's no Person.getGenderName?
|
||||
# It could be used by getDisplayInfo & this...
|
||||
if person.gender == Person.MALE:
|
||||
return [_("Men")]
|
||||
if person.gender == Person.FEMALE:
|
||||
return [_("Women")]
|
||||
return [_("Gender unknown")]
|
||||
|
||||
def get_year(self, event):
|
||||
"return year for given event"
|
||||
date = event.get_date_object()
|
||||
if date:
|
||||
year = date.get_year()
|
||||
if year:
|
||||
return [str(year)]
|
||||
return [_("Date(s) missing")]
|
||||
|
||||
def get_month(self, event):
|
||||
"return month for given event"
|
||||
date = event.get_date_object()
|
||||
if date:
|
||||
month = date.get_month()
|
||||
if month:
|
||||
return [DateHandler.displayer._months[month]]
|
||||
return [_("Date(s) missing")]
|
||||
|
||||
def get_place(self, event):
|
||||
"return place for given event"
|
||||
place_handle = event.get_place_handle()
|
||||
if place_handle:
|
||||
place = self.db.get_place_from_handle(place_handle).get_title()
|
||||
if place:
|
||||
return [place]
|
||||
return [_("Place missing")]
|
||||
|
||||
def get_places(self, data):
|
||||
"return places for given (person,event_handles)"
|
||||
places = []
|
||||
person, event_handles = data
|
||||
for event_handle in event_handles:
|
||||
event = self.db.get_event_from_handle(event_handle)
|
||||
place_handle = event.get_place_handle()
|
||||
if place_handle:
|
||||
place = self.db.get_place_from_handle(place_handle).get_title()
|
||||
if place:
|
||||
places.append(place)
|
||||
else:
|
||||
places.append(_("Place missing"))
|
||||
return places
|
||||
|
||||
def get_person_age(self, person):
|
||||
"return age for given person, if alive"
|
||||
death_ref = person.get_death_ref()
|
||||
if not death_ref:
|
||||
return [self.estimate_age(person)]
|
||||
return [_("Already dead")]
|
||||
|
||||
def get_death_age(self, person):
|
||||
"return age at death for given person, if dead"
|
||||
death_ref = person.get_death_ref()
|
||||
if death_ref:
|
||||
return [self.estimate_age(person, death_ref.ref)]
|
||||
return [_("Still alive")]
|
||||
|
||||
def get_event_ages(self, data):
|
||||
"return ages at given (person,event_handles)"
|
||||
ages = []
|
||||
person, event_handles = data
|
||||
for event_handle in event_handles:
|
||||
ages.append(self.estimate_age(person, event_handle))
|
||||
if ages:
|
||||
return ages
|
||||
return [_("Events missing")]
|
||||
|
||||
def get_event_type(self, data):
|
||||
"return event types at given (person,event_handles)"
|
||||
types = []
|
||||
person, event_handles = data
|
||||
for event_handle in event_handles:
|
||||
event = self.db.get_event_from_handle(event_handle)
|
||||
evtType = str(event.get_type())
|
||||
types.append(evtType)
|
||||
if types:
|
||||
return types
|
||||
return [_("Events missing")]
|
||||
|
||||
def get_first_child_age(self, data):
|
||||
"return age when first child in given (person,child_handles) was born"
|
||||
ages, errors = self.get_sorted_child_ages(data)
|
||||
if ages:
|
||||
errors.append(ages[0])
|
||||
return errors
|
||||
return [_("Children missing")]
|
||||
|
||||
def get_last_child_age(self, data):
|
||||
"return age when last child in given (person,child_handles) was born"
|
||||
ages, errors = self.get_sorted_child_ages(data)
|
||||
if ages:
|
||||
errors.append(ages[-1])
|
||||
return errors
|
||||
return [_("Children missing")]
|
||||
|
||||
def get_handle_count(self, data):
|
||||
"return number of handles in given (person, handle_list) used for child count, family count"
|
||||
return ["%3d" % len(data[1])]
|
||||
|
||||
# ------------------- utility methods -------------------------
|
||||
|
||||
def get_sorted_child_ages(self, data):
|
||||
"return (sorted_ages,errors) for given (person,child_handles)"
|
||||
ages = []
|
||||
errors = []
|
||||
person, child_handles = data
|
||||
for child_handle in child_handles:
|
||||
child = self.db.get_person_from_handle(child_handle)
|
||||
birth_ref = child.get_birth_ref()
|
||||
if birth_ref:
|
||||
ages.append(self.estimate_age(person, birth_ref.ref))
|
||||
else:
|
||||
errors.append(_("Birth missing"))
|
||||
continue
|
||||
ages.sort()
|
||||
return (ages, errors)
|
||||
|
||||
def estimate_age(self, person, end=None, begin=None):
|
||||
"""return estimated age (range) for given person or error message.
|
||||
age string is padded with spaces so that it can be sorted"""
|
||||
age = ReportUtils.estimate_age(self.db, person, end, begin)
|
||||
if age[0] < 0 or age[1] < 0:
|
||||
# inadequate information
|
||||
return _("Date(s) missing")
|
||||
if age[0] == age[1]:
|
||||
# exact year
|
||||
return "%3d" % age[0]
|
||||
else:
|
||||
# minimum and maximum
|
||||
return "%3d-%d" % (age[0], age[1])
|
||||
|
||||
# ------------------- type methods -------------------------
|
||||
# take db and person and return suitable gramps object(s)
|
||||
|
||||
def get_person(self, person):
|
||||
"return person"
|
||||
return person
|
||||
|
||||
def get_birth(self, person):
|
||||
"return birth event for given person or None"
|
||||
birth_ref = person.get_birth_ref()
|
||||
if birth_ref:
|
||||
return self.db.get_event_from_handle(birth_ref.ref)
|
||||
return None
|
||||
|
||||
def get_death(self, person):
|
||||
"return death event for given person or None"
|
||||
death_ref = person.get_death_ref()
|
||||
if death_ref:
|
||||
return self.db.get_event_from_handle(death_ref.ref)
|
||||
return None
|
||||
|
||||
def get_child_handles(self, person):
|
||||
"return list of child handles for given person or None"
|
||||
children = []
|
||||
for fam_handle in person.get_family_handle_list():
|
||||
fam = self.db.get_family_from_handle(fam_handle)
|
||||
for child_ref in fam.get_child_ref_list():
|
||||
children.append(child_ref.ref)
|
||||
# TODO: it would be good to return only biological children,
|
||||
# but GRAMPS doesn't offer any efficient way to check that
|
||||
# (I don't want to check each children's parent family mother
|
||||
# and father relations as that would make this *much* slower)
|
||||
if children:
|
||||
return (person, children)
|
||||
return None
|
||||
|
||||
def get_marriage_handles(self, person):
|
||||
"return list of marriage event handles for given person or None"
|
||||
marriages = []
|
||||
for family_handle in person.get_family_handle_list():
|
||||
family = self.db.get_family_from_handle(family_handle)
|
||||
if int(family.get_relationship()) == FamilyRelType.MARRIED:
|
||||
for event_ref in family.get_event_ref_list():
|
||||
event = self.db.get_event_from_handle(event_ref.ref)
|
||||
if event.type == EventType.MARRIAGE:
|
||||
marriages.append(event_ref.ref)
|
||||
if marriages:
|
||||
return (person, marriages)
|
||||
return None
|
||||
|
||||
def get_family_handles(self, person):
|
||||
"return list of family handles for given person or None"
|
||||
families = person.get_family_handle_list()
|
||||
|
||||
if families:
|
||||
return (person, families)
|
||||
return None
|
||||
|
||||
def get_event_handles(self, person):
|
||||
"return list of event handles for given person or None"
|
||||
events = []
|
||||
for event_ref in person.get_event_ref_list():
|
||||
events.append(event_ref.ref)
|
||||
|
||||
if events:
|
||||
return (person, events)
|
||||
return None
|
||||
|
||||
# ----------------- data collection methods --------------------
|
||||
|
||||
def get_person_data(self, person, collect):
|
||||
"""Add data from the database to 'collect' for the given person,
|
||||
using methods rom the 'collect' data dict tuple
|
||||
"""
|
||||
for chart in collect:
|
||||
# get the information
|
||||
type_func = chart[2]
|
||||
data_func = chart[3]
|
||||
obj = type_func(person) # e.g. get_date()
|
||||
if obj:
|
||||
value = data_func(obj) # e.g. get_year()
|
||||
else:
|
||||
value = [_("Personal information missing")]
|
||||
# list of information found
|
||||
for key in value:
|
||||
if key in chart[1].keys():
|
||||
chart[1][key] += 1
|
||||
else:
|
||||
chart[1][key] = 1
|
||||
|
||||
|
||||
def collect_data(self, db, filter_func, menu, genders,
|
||||
year_from, year_to, no_years):
|
||||
"""goes through the database and collects the selected personal
|
||||
data persons fitting the filter and birth year criteria. The
|
||||
arguments are:
|
||||
db - the GRAMPS database
|
||||
filter_func - filtering function selected by the StatisticsDialog
|
||||
options - report options_dict which sets which methods are used
|
||||
genders - which gender(s) to include into statistics
|
||||
year_from - use only persons who've born this year of after
|
||||
year_to - use only persons who've born this year or before
|
||||
no_years - use also people without known birth year
|
||||
|
||||
Returns an array of tuple of:
|
||||
- Extraction method title
|
||||
- Dict of values with their counts
|
||||
(- Method)
|
||||
"""
|
||||
self.db = db # store for use by methods
|
||||
|
||||
data = []
|
||||
ext = self.extractors
|
||||
# which methods to use
|
||||
for name in self.extractors.keys():
|
||||
option = menu.get_option_by_name(name)
|
||||
if option.get_value() == True:
|
||||
# localized data title, value dict, type and data method
|
||||
data.append((ext[name][1], {}, ext[name][2], ext[name][3]))
|
||||
|
||||
# go through the people and collect data
|
||||
for person_handle in filter_func.apply(db, db.get_person_handles(sort_handles=False)):
|
||||
|
||||
person = db.get_person_from_handle(person_handle)
|
||||
# check whether person has suitable gender
|
||||
if person.gender != genders and genders != Person.UNKNOWN:
|
||||
continue
|
||||
|
||||
# check whether birth year is within required range
|
||||
birth = self.get_birth(person)
|
||||
if birth:
|
||||
birthdate = birth.get_date_object()
|
||||
if birthdate.get_year_valid():
|
||||
year = birthdate.get_year()
|
||||
if not (year >= year_from and year <= year_to):
|
||||
continue
|
||||
else:
|
||||
# if death before range, person's out of range too...
|
||||
death = self.get_death(person)
|
||||
if death:
|
||||
deathdate = death.get_date_object()
|
||||
if deathdate.get_year_valid() and deathdate.get_year() < year_from:
|
||||
continue
|
||||
if not no_years:
|
||||
# do not accept people who are not known to be in range
|
||||
continue
|
||||
else:
|
||||
continue
|
||||
else:
|
||||
continue
|
||||
|
||||
self.get_person_data(person, data)
|
||||
return data
|
||||
|
||||
# GLOBAL: required so that we get access to _Extract.extractors[]
|
||||
# Unfortunately class variables cannot reference instance methods :-/
|
||||
_Extract = Extract()
|
||||
|
||||
#------------------------------------------------------------------------
|
||||
#
|
||||
# Statistics report
|
||||
#
|
||||
#------------------------------------------------------------------------
|
||||
class StatisticsChart(Report):
|
||||
|
||||
def __init__(self, database, options_class):
|
||||
"""
|
||||
Create the Statistics object that produces the report.
|
||||
Uses the Extractor class to extract the data from the database.
|
||||
|
||||
The arguments are:
|
||||
|
||||
database - the GRAMPS database instance
|
||||
person - currently selected person
|
||||
options_class - instance of the Options class for this report
|
||||
|
||||
To see what the options are, check the options help in the options class.
|
||||
"""
|
||||
Report.__init__(self, database, options_class)
|
||||
menu = options_class.menu
|
||||
|
||||
self.filter_option = menu.get_option_by_name('filter')
|
||||
self.filter = self.filter_option.get_filter()
|
||||
|
||||
self.bar_items = menu.get_option_by_name('bar_items').get_value()
|
||||
year_from = menu.get_option_by_name('year_from').get_value()
|
||||
year_to = menu.get_option_by_name('year_to').get_value()
|
||||
gender = menu.get_option_by_name('gender').get_value()
|
||||
|
||||
# title needs both data extraction method name + gender name
|
||||
if gender == Person.MALE:
|
||||
genders = _("Men")
|
||||
elif gender == Person.FEMALE:
|
||||
genders = _("Women")
|
||||
else:
|
||||
genders = None
|
||||
|
||||
# needed for keyword based localization
|
||||
mapping = {
|
||||
'genders': genders,
|
||||
'year_from': year_from,
|
||||
'year_to': year_to
|
||||
}
|
||||
self.progress = ProgressMeter(_('Statistics Charts'))
|
||||
|
||||
# extract requested items from the database and count them
|
||||
self.progress.set_pass(_('Collecting data...'), 1)
|
||||
tables = _Extract.collect_data(database, self.filter, menu,
|
||||
gender, year_from, year_to,
|
||||
menu.get_option_by_name('no_years').get_value())
|
||||
self.progress.step()
|
||||
|
||||
self.progress.set_pass(_('Sorting data...'), len(tables))
|
||||
self.data = []
|
||||
sortby = menu.get_option_by_name('sortby').get_value()
|
||||
reverse = menu.get_option_by_name('reverse').get_value()
|
||||
for table in tables:
|
||||
# generate sorted item lookup index index
|
||||
lookup = self.index_items(table[1], sortby, reverse)
|
||||
# document heading
|
||||
mapping['chart_title'] = table[0]
|
||||
if genders:
|
||||
heading = _("%(genders)s born %(year_from)04d-%(year_to)04d: %(chart_title)s") % mapping
|
||||
else:
|
||||
heading = _("Persons born %(year_from)04d-%(year_to)04d: %(chart_title)s") % mapping
|
||||
self.data.append((heading, table[0], table[1], lookup))
|
||||
self.progress.step()
|
||||
#DEBUG
|
||||
#print heading
|
||||
#print table[1]
|
||||
|
||||
|
||||
def lookup_compare(self, a, b):
|
||||
"compare given keys according to corresponding lookup values"
|
||||
return cmp(self.lookup_items[a], self.lookup_items[b])
|
||||
|
||||
def index_items(self, data, sort, reverse):
|
||||
"""creates & stores a sorted index for the items"""
|
||||
|
||||
# sort by item keys
|
||||
index = data.keys()
|
||||
index.sort()
|
||||
if reverse:
|
||||
index.reverse()
|
||||
|
||||
if sort == _options.SORT_VALUE:
|
||||
# set for the sorting function
|
||||
self.lookup_items = data
|
||||
|
||||
# then sort by value
|
||||
index.sort(self.lookup_compare)
|
||||
if reverse:
|
||||
index.reverse()
|
||||
|
||||
return index
|
||||
|
||||
def write_report(self):
|
||||
"output the selected statistics..."
|
||||
|
||||
self.progress.set_pass(_('Saving charts...'), len(self.data))
|
||||
for data in self.data:
|
||||
self.doc.start_page()
|
||||
if len(data[2]) < self.bar_items:
|
||||
self.output_piechart(data[0], data[1], data[2], data[3])
|
||||
else:
|
||||
self.output_barchart(data[0], data[1], data[2], data[3])
|
||||
self.doc.end_page()
|
||||
self.progress.step()
|
||||
self.progress.close()
|
||||
|
||||
|
||||
def output_piechart(self, title, typename, data, lookup):
|
||||
|
||||
# set layout variables
|
||||
middle_w = self.doc.get_usable_width() / 2
|
||||
middle_h = self.doc.get_usable_height() / 2
|
||||
middle = min(middle_w,middle_h)
|
||||
|
||||
# start output
|
||||
self.doc.center_text('SC-title', title, middle_w, 0)
|
||||
style_sheet = self.doc.get_style_sheet()
|
||||
pstyle = style_sheet.get_paragraph_style('SC-Title')
|
||||
yoffset = ReportUtils.pt2cm(pstyle.get_font().get_size())
|
||||
|
||||
# collect data for output
|
||||
color = 0
|
||||
chart_data = []
|
||||
for key in lookup:
|
||||
style = "SC-color-%d" % color
|
||||
text = "%s (%d)" % (key, data[key])
|
||||
# graphics style, value, and it's label
|
||||
chart_data.append((style, data[key], text))
|
||||
color = (color+1) % 7 # There are only 7 color styles defined
|
||||
|
||||
margin = 1.0
|
||||
legendx = 2.0
|
||||
|
||||
# output data...
|
||||
radius = middle - 2*margin
|
||||
yoffset = yoffset + margin + radius
|
||||
ReportUtils.draw_pie_chart(self.doc, middle_w, yoffset, radius, chart_data, -90)
|
||||
yoffset = yoffset + radius + 2*margin
|
||||
if middle == middle_h: # Landscape
|
||||
legendx = 1.0
|
||||
yoffset = margin
|
||||
|
||||
text = _("%s (persons):") % typename
|
||||
ReportUtils.draw_legend(self.doc, legendx, yoffset, chart_data, text,'SC-legend')
|
||||
|
||||
|
||||
def output_barchart(self, title, typename, data, lookup):
|
||||
|
||||
pt2cm = ReportUtils.pt2cm
|
||||
style_sheet = self.doc.get_style_sheet()
|
||||
pstyle = style_sheet.get_paragraph_style('SC-Text')
|
||||
font = pstyle.get_font()
|
||||
|
||||
# set layout variables
|
||||
width = self.doc.get_usable_width()
|
||||
row_h = pt2cm(font.get_size())
|
||||
max_y = self.doc.get_usable_height() - row_h
|
||||
pad = row_h * 0.5
|
||||
|
||||
# check maximum value
|
||||
max_value = 0
|
||||
for key in lookup:
|
||||
max_value = max(data[key], max_value)
|
||||
# horizontal area for the gfx bars
|
||||
margin = 1.0
|
||||
middle = width/2.0
|
||||
textx = middle + margin/2.0
|
||||
stopx = middle - margin/2.0
|
||||
maxsize = stopx - margin
|
||||
|
||||
# start output
|
||||
self.doc.center_text('SC-title', title, middle, 0)
|
||||
pstyle = style_sheet.get_paragraph_style('SC-Title')
|
||||
yoffset = pt2cm(pstyle.get_font().get_size())
|
||||
#print title
|
||||
|
||||
# header
|
||||
yoffset += (row_h + pad)
|
||||
text = _("%s (persons):") % typename
|
||||
self.doc.draw_text('SC-text', text, textx, yoffset)
|
||||
|
||||
for key in lookup:
|
||||
yoffset += (row_h + pad)
|
||||
if yoffset > max_y:
|
||||
# for graphical report, page_break() doesn't seem to work
|
||||
self.doc.end_page()
|
||||
self.doc.start_page()
|
||||
yoffset = 0
|
||||
|
||||
# right align bar to the text
|
||||
value = data[key]
|
||||
startx = stopx - (maxsize * value / max_value)
|
||||
self.doc.draw_box('SC-bar',"",startx,yoffset,stopx-startx,row_h)
|
||||
# text after bar
|
||||
text = "%s (%d)" % (key, data[key])
|
||||
self.doc.draw_text('SC-text', text, textx, yoffset)
|
||||
#print key + ":",
|
||||
|
||||
return
|
||||
|
||||
#------------------------------------------------------------------------
|
||||
#
|
||||
# StatisticsChartOptions
|
||||
#
|
||||
#------------------------------------------------------------------------
|
||||
class StatisticsChartOptions(MenuReportOptions):
|
||||
|
||||
def __init__(self, name, dbase):
|
||||
self.__pid = None
|
||||
self.__filter = None
|
||||
self.__db = dbase
|
||||
MenuReportOptions.__init__(self, name, dbase)
|
||||
|
||||
def add_menu_options(self, menu):
|
||||
"""
|
||||
Add options to the menu for the statistics report.
|
||||
"""
|
||||
category_name = _("Report Options")
|
||||
|
||||
self.__filter = FilterOption(_("Filter"), 0)
|
||||
self.__filter.set_help(
|
||||
_("Determines what people are included in the report"))
|
||||
menu.add_option(category_name, "filter", self.__filter)
|
||||
self.__filter.connect('value-changed', self.__filter_changed)
|
||||
|
||||
self.__pid = PersonOption(_("Filter Person"))
|
||||
self.__pid.set_help(_("The center person for the filter"))
|
||||
menu.add_option(category_name, "pid", self.__pid)
|
||||
self.__pid.connect('value-changed', self.__update_filters)
|
||||
|
||||
self.__update_filters()
|
||||
|
||||
sortby = EnumeratedListOption(_('Sort chart items by'),
|
||||
_options.SORT_VALUE )
|
||||
for item_idx in range(len(_options.sorts)):
|
||||
item = _options.sorts[item_idx]
|
||||
sortby.add_item(item_idx,item[2])
|
||||
sortby.set_help( _("Select how the statistical data is sorted."))
|
||||
menu.add_option(category_name,"sortby",sortby)
|
||||
|
||||
reverse = BooleanOption(_("Sort in reverse order"), False)
|
||||
reverse.set_help(_("Check to reverse the sorting order."))
|
||||
menu.add_option(category_name,"reverse", reverse)
|
||||
|
||||
this_year = time.localtime()[0]
|
||||
year_from = NumberOption(_("People Born After"),
|
||||
1700, 1, this_year)
|
||||
year_from.set_help(_("Birth year from which to include people"))
|
||||
menu.add_option(category_name,"year_from", year_from)
|
||||
|
||||
year_to = NumberOption(_("People Born Before"),
|
||||
this_year, 1, this_year)
|
||||
year_to.set_help(_("Birth year until which to include people"))
|
||||
menu.add_option(category_name,"year_to", year_to)
|
||||
|
||||
no_years = BooleanOption(_("Include people without known birth years"),
|
||||
False)
|
||||
no_years.set_help(_("Whether to include people without "
|
||||
"known birth years"))
|
||||
menu.add_option(category_name,"no_years", no_years)
|
||||
|
||||
gender = EnumeratedListOption(_('Genders included'),
|
||||
Person.UNKNOWN )
|
||||
for item_idx in range(len(_options.genders)):
|
||||
item = _options.genders[item_idx]
|
||||
gender.add_item(item[0],item[2])
|
||||
gender.set_help( _("Select which genders are included into "
|
||||
"statistics."))
|
||||
menu.add_option(category_name,"gender",gender)
|
||||
|
||||
bar_items = NumberOption(_("Max. items for a pie"), 8, 0, 20)
|
||||
bar_items.set_help(_("With fewer items pie chart and legend will be "
|
||||
"used instead of a bar chart."))
|
||||
menu.add_option(category_name,"bar_items", bar_items)
|
||||
|
||||
# -------------------------------------------------
|
||||
# List of available charts on separate option tabs
|
||||
idx = 0
|
||||
half = (len(_Extract.extractors))/2
|
||||
self.charts = {}
|
||||
for key in _Extract.extractors:
|
||||
if idx < half:
|
||||
category_name = _("Charts 1")
|
||||
else:
|
||||
category_name = _("Charts 2")
|
||||
|
||||
opt = BooleanOption(_Extract.extractors[key][1], False)
|
||||
opt.set_help(_("Include charts with indicated data"))
|
||||
menu.add_option(category_name,key, opt)
|
||||
idx += 1
|
||||
|
||||
# Enable a couple of charts by default
|
||||
menu.get_option_by_name("data_gender").set_value(True)
|
||||
menu.get_option_by_name("data_ccount").set_value(True)
|
||||
menu.get_option_by_name("data_bmonth").set_value(True)
|
||||
|
||||
def __update_filters(self):
|
||||
"""
|
||||
Update the filter list based on the selected person
|
||||
"""
|
||||
gid = self.__pid.get_value()
|
||||
person = self.__db.get_person_from_gramps_id(gid)
|
||||
filter_list = ReportUtils.get_person_filters(person, False)
|
||||
self.__filter.set_filters(filter_list)
|
||||
|
||||
def __filter_changed(self):
|
||||
"""
|
||||
Handle filter change. If the filter is not specific to a person,
|
||||
disable the person option
|
||||
"""
|
||||
filter_value = self.__filter.get_value()
|
||||
if filter_value in [1, 2, 3, 4]:
|
||||
# Filters 1, 2, 3 and 4 rely on the center person
|
||||
self.__pid.set_available(True)
|
||||
else:
|
||||
# The rest don't
|
||||
self.__pid.set_available(False)
|
||||
|
||||
def make_default_style(self, default_style):
|
||||
"""Make the default output style for the Statistics report."""
|
||||
# Paragraph Styles
|
||||
f = BaseDoc.FontStyle()
|
||||
f.set_size(10)
|
||||
f.set_type_face(BaseDoc.FONT_SERIF)
|
||||
p = BaseDoc.ParagraphStyle()
|
||||
p.set_font(f)
|
||||
p.set_alignment(BaseDoc.PARA_ALIGN_LEFT)
|
||||
p.set_description(_("The style used for the items and values."))
|
||||
default_style.add_paragraph_style("SC-Text",p)
|
||||
|
||||
f = BaseDoc.FontStyle()
|
||||
f.set_size(14)
|
||||
f.set_type_face(BaseDoc.FONT_SANS_SERIF)
|
||||
p = BaseDoc.ParagraphStyle()
|
||||
p.set_font(f)
|
||||
p.set_alignment(BaseDoc.PARA_ALIGN_CENTER)
|
||||
p.set_description(_("The style used for the title of the page."))
|
||||
default_style.add_paragraph_style("SC-Title",p)
|
||||
|
||||
"""
|
||||
Graphic Styles:
|
||||
SC-title - Contains the SC-Title paragraph style used for
|
||||
the title of the document
|
||||
SC-text - Contains the SC-Name paragraph style used for
|
||||
the individual's name
|
||||
SC-color-N - The colors for drawing pies.
|
||||
SC-bar - A red bar with 0.5pt black line.
|
||||
"""
|
||||
g = BaseDoc.GraphicsStyle()
|
||||
g.set_paragraph_style("SC-Title")
|
||||
g.set_color((0,0,0))
|
||||
g.set_fill_color((255,255,255))
|
||||
g.set_line_width(0)
|
||||
default_style.add_draw_style("SC-title",g)
|
||||
|
||||
g = BaseDoc.GraphicsStyle()
|
||||
g.set_paragraph_style("SC-Text")
|
||||
g.set_color((0,0,0))
|
||||
g.set_fill_color((255,255,255))
|
||||
g.set_line_width(0)
|
||||
default_style.add_draw_style("SC-text",g)
|
||||
|
||||
width = 0.8
|
||||
# red
|
||||
g = BaseDoc.GraphicsStyle()
|
||||
g.set_paragraph_style('SC-Text')
|
||||
g.set_color((0,0,0))
|
||||
g.set_fill_color((255,0,0))
|
||||
g.set_line_width(width)
|
||||
default_style.add_draw_style("SC-color-0",g)
|
||||
# orange
|
||||
g = BaseDoc.GraphicsStyle()
|
||||
g.set_paragraph_style('SC-Text')
|
||||
g.set_color((0,0,0))
|
||||
g.set_fill_color((255,158,33))
|
||||
g.set_line_width(width)
|
||||
default_style.add_draw_style("SC-color-1",g)
|
||||
# green
|
||||
g = BaseDoc.GraphicsStyle()
|
||||
g.set_paragraph_style('SC-Text')
|
||||
g.set_color((0,0,0))
|
||||
g.set_fill_color((0,178,0))
|
||||
g.set_line_width(width)
|
||||
default_style.add_draw_style("SC-color-2",g)
|
||||
# violet
|
||||
g = BaseDoc.GraphicsStyle()
|
||||
g.set_paragraph_style('SC-Text')
|
||||
g.set_color((0,0,0))
|
||||
g.set_fill_color((123,0,123))
|
||||
g.set_line_width(width)
|
||||
default_style.add_draw_style("SC-color-3",g)
|
||||
# yellow
|
||||
g = BaseDoc.GraphicsStyle()
|
||||
g.set_paragraph_style('SC-Text')
|
||||
g.set_color((0,0,0))
|
||||
g.set_fill_color((255,255,0))
|
||||
g.set_line_width(width)
|
||||
default_style.add_draw_style("SC-color-4",g)
|
||||
# blue
|
||||
g = BaseDoc.GraphicsStyle()
|
||||
g.set_paragraph_style('SC-Text')
|
||||
g.set_color((0,0,0))
|
||||
g.set_fill_color((0,105,214))
|
||||
g.set_line_width(width)
|
||||
default_style.add_draw_style("SC-color-5",g)
|
||||
# gray
|
||||
g = BaseDoc.GraphicsStyle()
|
||||
g.set_paragraph_style('SC-Text')
|
||||
g.set_color((0,0,0))
|
||||
g.set_fill_color((210,204,210))
|
||||
g.set_line_width(width)
|
||||
default_style.add_draw_style("SC-color-6",g)
|
||||
|
||||
g = BaseDoc.GraphicsStyle()
|
||||
g.set_color((0,0,0))
|
||||
g.set_fill_color((255,0,0))
|
||||
g.set_line_width(width)
|
||||
default_style.add_draw_style("SC-bar",g)
|
||||
|
||||
# legend
|
||||
g = BaseDoc.GraphicsStyle()
|
||||
g.set_paragraph_style('SC-Text')
|
||||
g.set_color((0,0,0))
|
||||
g.set_fill_color((255,255,255))
|
||||
g.set_line_width(0)
|
||||
default_style.add_draw_style("SC-legend",g)
|
||||
|
||||
#------------------------------------------------------------------------
|
||||
#
|
||||
# Register report/options
|
||||
#
|
||||
#------------------------------------------------------------------------
|
||||
pmgr = PluginManager.get_instance()
|
||||
pmgr.register_report(
|
||||
name = 'statistics_chart',
|
||||
category = CATEGORY_DRAW,
|
||||
report_class = StatisticsChart,
|
||||
options_class = StatisticsChartOptions,
|
||||
modes = PluginManager.REPORT_MODE_GUI | \
|
||||
PluginManager.REPORT_MODE_BKI | \
|
||||
PluginManager.REPORT_MODE_CLI,
|
||||
translated_name = _("Statistics Charts"),
|
||||
status = _("Stable"),
|
||||
author_name = "Eero Tamminen",
|
||||
author_email = "",
|
||||
description = _("Produces statistical bar and pie charts of the people "
|
||||
"in the database"),
|
||||
require_active = False,
|
||||
)
|
||||
470
src/plugins/drawreport/TimeLine.py
Normal file
470
src/plugins/drawreport/TimeLine.py
Normal file
@@ -0,0 +1,470 @@
|
||||
#
|
||||
# Gramps - a GTK+/GNOME based genealogy program
|
||||
#
|
||||
# Copyright (C) 2003-2007 Donald N. Allingham
|
||||
# Copyright (C) 2007-2008 Brian G. Matherly
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation; either version 2 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||
#
|
||||
|
||||
# $Id$
|
||||
|
||||
"""
|
||||
Timeline Chart
|
||||
"""
|
||||
|
||||
#------------------------------------------------------------------------
|
||||
#
|
||||
# python modules
|
||||
#
|
||||
#------------------------------------------------------------------------
|
||||
from TransUtils import sgettext as _
|
||||
|
||||
#------------------------------------------------------------------------
|
||||
#
|
||||
# GRAMPS modules
|
||||
#
|
||||
#------------------------------------------------------------------------
|
||||
from gen.plug import PluginManager
|
||||
from gen.plug.menu import PersonOption, FilterOption, EnumeratedListOption
|
||||
from ReportBase import Report, ReportUtils, MenuReportOptions, CATEGORY_DRAW
|
||||
pt2cm = ReportUtils.pt2cm
|
||||
import BaseDoc
|
||||
import Sort
|
||||
from QuestionDialog import ErrorDialog
|
||||
from BasicUtils import name_displayer
|
||||
from Utils import probably_alive, ProgressMeter
|
||||
import gen.lib
|
||||
|
||||
#------------------------------------------------------------------------
|
||||
#
|
||||
# Private Functions
|
||||
#
|
||||
#------------------------------------------------------------------------
|
||||
def _get_sort_functions(sort):
|
||||
return [
|
||||
(_("Birth Date"),sort.by_birthdate),
|
||||
(_("Name"),sort.by_last_name),
|
||||
]
|
||||
|
||||
#------------------------------------------------------------------------
|
||||
#
|
||||
# TimeLine
|
||||
#
|
||||
#------------------------------------------------------------------------
|
||||
class TimeLine(Report):
|
||||
|
||||
def __init__(self, database, options_class):
|
||||
"""
|
||||
Create the Timeline object that produces the report.
|
||||
|
||||
The arguments are:
|
||||
|
||||
database - the GRAMPS database instance
|
||||
person - currently selected person
|
||||
options_class - instance of the Options class for this report
|
||||
|
||||
This report needs the following parameters (class variables)
|
||||
that come in the options class.
|
||||
|
||||
filter - Filter to be applied to the people of the database.
|
||||
The option class carries its number, and the function
|
||||
returning the list of filters.
|
||||
sortby - Sorting method to be used.
|
||||
"""
|
||||
|
||||
Report.__init__(self, database, options_class)
|
||||
menu = options_class.menu
|
||||
self.filter = menu.get_option_by_name('filter').get_filter()
|
||||
|
||||
self.title = _("Timeline Graph for %s") % self.filter.get_name()
|
||||
|
||||
sort_func_num = menu.get_option_by_name('sortby').get_value()
|
||||
sort_functions = _get_sort_functions(Sort.Sort(database))
|
||||
self.sort_name = sort_functions[sort_func_num][0]
|
||||
self.sort_func = sort_functions[sort_func_num][1]
|
||||
self.calendar = gen.lib.date.Date.ui_calendar_names[menu.get_option_by_name('calendar').get_value()]
|
||||
|
||||
def write_report(self):
|
||||
self.progress = ProgressMeter(_('Timeline'))
|
||||
|
||||
(low, high) = self.find_year_range()
|
||||
|
||||
if low == high:
|
||||
if self.standalone:
|
||||
self.doc.close()
|
||||
ErrorDialog(_("Report could not be created"),
|
||||
_("The range of dates chosen was not valid"))
|
||||
return
|
||||
|
||||
st_size = self.name_size()
|
||||
|
||||
style_sheet = self.doc.get_style_sheet()
|
||||
font = style_sheet.get_paragraph_style('TLG-Name').get_font()
|
||||
|
||||
incr = pt2cm(font.get_size())
|
||||
pad = incr*.75
|
||||
|
||||
x1,x2,y1,y2 = (0,0,0,0)
|
||||
|
||||
start = st_size+0.5
|
||||
stop = self.doc.get_usable_width()-0.5
|
||||
size = (stop-start)
|
||||
self.header = 2.0
|
||||
|
||||
self.doc.start_page()
|
||||
|
||||
index = 1
|
||||
current = 1;
|
||||
|
||||
length = len(self.plist)
|
||||
|
||||
self.progress.set_pass(_('Sorting dates...'), 1)
|
||||
self.plist.sort(self.sort_func)
|
||||
self.progress.set_pass(_('Calculating timeline...'), len(self.plist))
|
||||
|
||||
for p_id in self.plist:
|
||||
self.progress.step()
|
||||
p = self.database.get_person_from_handle(p_id)
|
||||
birth = ReportUtils.get_birth_or_fallback(self.database, p)
|
||||
if birth:
|
||||
b = birth.get_date_object().to_calendar(self.calendar).get_year()
|
||||
else:
|
||||
b = None
|
||||
|
||||
death = ReportUtils.get_death_or_fallback(self.database, p)
|
||||
if death:
|
||||
d = death.get_date_object().to_calendar(self.calendar).get_year()
|
||||
else:
|
||||
d = None
|
||||
|
||||
n = name_displayer.display_formal(p)
|
||||
self.doc.draw_text('TLG-text', n,incr+pad,
|
||||
self.header + (incr+pad)*index)
|
||||
|
||||
y1 = self.header + (pad+incr)*index
|
||||
y2 = self.header + ((pad+incr)*index)+incr
|
||||
y3 = (y1+y2)/2.0
|
||||
w = 0.05
|
||||
|
||||
if b:
|
||||
start_offset = ((float(b-low)/float(high-low)) * (size))
|
||||
x1 = start+start_offset
|
||||
path = [(x1,y1),(x1+w,y3),(x1,y2),(x1-w,y3)]
|
||||
self.doc.draw_path('TLG-line',path)
|
||||
|
||||
if d:
|
||||
start_offset = ((float(d-low)/float(high-low)) * (size))
|
||||
x1 = start+start_offset
|
||||
path = [(x1,y1),(x1+w,y3),(x1,y2),(x1-w,y3)]
|
||||
self.doc.draw_path('TLG-solid',path)
|
||||
|
||||
if b and d:
|
||||
start_offset = ((float(b-low)/float(high-low)) * size) + w
|
||||
stop_offset = ((float(d-low)/float(high-low)) * size) - w
|
||||
|
||||
x1 = start+start_offset
|
||||
x2 = start+stop_offset
|
||||
self.doc.draw_line('open',x1,y3,x2,y3)
|
||||
|
||||
if (y2 + incr) >= self.doc.get_usable_height():
|
||||
if current != length:
|
||||
self.build_grid(low, high,start,stop)
|
||||
self.doc.end_page()
|
||||
self.doc.start_page()
|
||||
self.build_grid(low, high,start,stop)
|
||||
index = 1
|
||||
x1,x2,y1,y2 = (0,0,0,0)
|
||||
else:
|
||||
index += 1;
|
||||
current += 1
|
||||
self.progress.close()
|
||||
self.build_grid(low, high,start,stop)
|
||||
self.doc.end_page()
|
||||
|
||||
def build_grid(self,year_low,year_high,start_pos,stop_pos):
|
||||
"""
|
||||
Draws the grid outline for the chart. Sets the document label,
|
||||
draws the vertical lines, and adds the year labels. Arguments
|
||||
are:
|
||||
|
||||
year_low - lowest year on the chart
|
||||
year_high - highest year on the chart
|
||||
start_pos - x position of the lowest leftmost grid line
|
||||
stop_pos - x position of the rightmost grid line
|
||||
"""
|
||||
width = self.doc.get_usable_width()
|
||||
|
||||
style_sheet = self.doc.get_style_sheet()
|
||||
title_font = style_sheet.get_paragraph_style('TLG-Title').get_font()
|
||||
normal_font = style_sheet.get_paragraph_style('TLG-Name').get_font()
|
||||
label_font = style_sheet.get_paragraph_style('TLG-Label').get_font()
|
||||
|
||||
byline = _("%(calendar_type)s Calendar, Sorted by %(sortby)s" %
|
||||
{"calendar_type": self.calendar,
|
||||
"sortby": self.sort_name})
|
||||
|
||||
self.doc.center_text('TLG-title',self.title + "\n" + byline,width/2.0,0)
|
||||
|
||||
label_y = self.header - (pt2cm(normal_font.get_size())*1.2)
|
||||
top_y = self.header
|
||||
bottom_y = self.doc.get_usable_height()
|
||||
|
||||
incr = (year_high - year_low)/5
|
||||
delta = (stop_pos - start_pos)/ 5
|
||||
|
||||
for val in range(0,6):
|
||||
year_str = str(year_low + (incr*val))
|
||||
|
||||
xpos = start_pos+(val*delta)
|
||||
self.doc.center_text('TLG-label', year_str, xpos, label_y)
|
||||
self.doc.draw_line('TLG-grid', xpos, top_y, xpos, bottom_y)
|
||||
|
||||
def find_year_range(self):
|
||||
low = 999999
|
||||
high = -999999
|
||||
|
||||
self.plist = self.filter.apply(self.database,
|
||||
self.database.get_person_handles(sort_handles=False))
|
||||
|
||||
for p_id in self.plist:
|
||||
p = self.database.get_person_from_handle(p_id)
|
||||
birth = ReportUtils.get_birth_or_fallback(self.database, p)
|
||||
if birth:
|
||||
b = birth.get_date_object().to_calendar(self.calendar).get_year()
|
||||
else:
|
||||
b = None
|
||||
|
||||
death = ReportUtils.get_death_or_fallback(self.database, p)
|
||||
if death:
|
||||
d = death.get_date_object().to_calendar(self.calendar).get_year()
|
||||
else:
|
||||
d = None
|
||||
|
||||
if b:
|
||||
low = min(low,b)
|
||||
high = max(high,b)
|
||||
|
||||
if d:
|
||||
low = min(low,d)
|
||||
high = max(high,d)
|
||||
|
||||
# round the dates to the nearest decade
|
||||
low = int((low/10))*10
|
||||
high = int(((high+9)/10))*10
|
||||
|
||||
# Make sure the difference is a multiple of 50 so all year ranges land
|
||||
# on a decade.
|
||||
low -= 50 - ((high-low) % 50)
|
||||
|
||||
if low is None:
|
||||
low = high
|
||||
if high is None:
|
||||
high = low
|
||||
|
||||
return (low, high)
|
||||
|
||||
def name_size(self):
|
||||
self.plist = self.filter.apply(self.database,
|
||||
self.database.get_person_handles(sort_handles=False))
|
||||
|
||||
style_sheet = self.doc.get_style_sheet()
|
||||
gstyle = style_sheet.get_draw_style('TLG-text')
|
||||
pname = gstyle.get_paragraph_style()
|
||||
pstyle = style_sheet.get_paragraph_style(pname)
|
||||
font = pstyle.get_font()
|
||||
|
||||
size = 0
|
||||
for p_id in self.plist:
|
||||
p = self.database.get_person_from_handle(p_id)
|
||||
n = name_displayer.display_formal(p)
|
||||
size = max(self.doc.string_width(font, n),size)
|
||||
return pt2cm(size)
|
||||
|
||||
#------------------------------------------------------------------------
|
||||
#
|
||||
# TimeLineOptions
|
||||
#
|
||||
#------------------------------------------------------------------------
|
||||
class TimeLineOptions(MenuReportOptions):
|
||||
|
||||
def __init__(self, name, dbase):
|
||||
self.__pid = None
|
||||
self.__filter = None
|
||||
self.__db = dbase
|
||||
MenuReportOptions.__init__(self, name, dbase)
|
||||
|
||||
def add_menu_options(self, menu):
|
||||
category_name = _("Report Options")
|
||||
|
||||
self.__filter = FilterOption(_("Filter"), 0)
|
||||
self.__filter.set_help(
|
||||
_("Determines what people are included in the report"))
|
||||
menu.add_option(category_name, "filter", self.__filter)
|
||||
self.__filter.connect('value-changed', self.__filter_changed)
|
||||
|
||||
self.__pid = PersonOption(_("Filter Person"))
|
||||
self.__pid.set_help(_("The center person for the filter"))
|
||||
menu.add_option(category_name, "pid", self.__pid)
|
||||
self.__pid.connect('value-changed', self.__update_filters)
|
||||
|
||||
self.__update_filters()
|
||||
|
||||
sortby = EnumeratedListOption(_('Sort by'), 0 )
|
||||
idx = 0
|
||||
for item in _get_sort_functions(Sort.Sort(self.__db)):
|
||||
sortby.add_item(idx,item[0])
|
||||
idx += 1
|
||||
sortby.set_help( _("Sorting method to use"))
|
||||
menu.add_option(category_name,"sortby",sortby)
|
||||
|
||||
self.__calendar = EnumeratedListOption(_("Calendar"), 0)
|
||||
self.__calendar.set_help(_("The calendar which determines the year span"))
|
||||
idx = 0
|
||||
for calendar in gen.lib.date.Date.ui_calendar_names:
|
||||
self.__calendar.add_item(idx, calendar)
|
||||
idx += 1
|
||||
menu.add_option(category_name, "calendar", self.__calendar)
|
||||
|
||||
def __update_filters(self):
|
||||
"""
|
||||
Update the filter list based on the selected person
|
||||
"""
|
||||
gid = self.__pid.get_value()
|
||||
person = self.__db.get_person_from_gramps_id(gid)
|
||||
filter_list = ReportUtils.get_person_filters(person, False)
|
||||
self.__filter.set_filters(filter_list)
|
||||
|
||||
def __filter_changed(self):
|
||||
"""
|
||||
Handle filter change. If the filter is not specific to a person,
|
||||
disable the person option
|
||||
"""
|
||||
filter_value = self.__filter.get_value()
|
||||
if filter_value in [1, 2, 3, 4]:
|
||||
# Filters 1, 2, 3 and 4 rely on the center person
|
||||
self.__pid.set_available(True)
|
||||
else:
|
||||
# The rest don't
|
||||
self.__pid.set_available(False)
|
||||
|
||||
def make_default_style(self,default_style):
|
||||
"""Make the default output style for the Timeline report."""
|
||||
# Paragraph Styles
|
||||
f = BaseDoc.FontStyle()
|
||||
f.set_size(10)
|
||||
f.set_type_face(BaseDoc.FONT_SANS_SERIF)
|
||||
p = BaseDoc.ParagraphStyle()
|
||||
p.set_font(f)
|
||||
p.set_description(_("The style used for the person's name."))
|
||||
default_style.add_paragraph_style("TLG-Name",p)
|
||||
|
||||
f = BaseDoc.FontStyle()
|
||||
f.set_size(8)
|
||||
f.set_type_face(BaseDoc.FONT_SANS_SERIF)
|
||||
p = BaseDoc.ParagraphStyle()
|
||||
p.set_font(f)
|
||||
p.set_alignment(BaseDoc.PARA_ALIGN_CENTER)
|
||||
p.set_description(_("The style used for the year labels."))
|
||||
default_style.add_paragraph_style("TLG-Label",p)
|
||||
|
||||
f = BaseDoc.FontStyle()
|
||||
f.set_size(14)
|
||||
f.set_type_face(BaseDoc.FONT_SANS_SERIF)
|
||||
p = BaseDoc.ParagraphStyle()
|
||||
p.set_font(f)
|
||||
p.set_alignment(BaseDoc.PARA_ALIGN_CENTER)
|
||||
p.set_description(_("The style used for the title of the page."))
|
||||
default_style.add_paragraph_style("TLG-Title",p)
|
||||
|
||||
"""
|
||||
Graphic Styles
|
||||
TLG-grid - 0.5pt wide line dashed line. Used for the lines that
|
||||
make up the grid.
|
||||
TLG-line - 0.5pt wide line. Used for the line connecting two
|
||||
endpoints and for the birth marker.
|
||||
TLG-solid - 0.5pt line with a black fill color. Used for the date of
|
||||
death marker.
|
||||
TLG-text - Contains the TLG-Name paragraph style used for the
|
||||
individual's name.
|
||||
TLG-title - Contains the TLG-Title paragraph style used for the
|
||||
title of the document.
|
||||
TLG-label - Contains the TLG-Label paragraph style used for the year
|
||||
label's in the document.
|
||||
"""
|
||||
g = BaseDoc.GraphicsStyle()
|
||||
g.set_line_width(0.5)
|
||||
g.set_color((0,0,0))
|
||||
default_style.add_draw_style("TLG-line",g)
|
||||
|
||||
g = BaseDoc.GraphicsStyle()
|
||||
g.set_line_width(0.5)
|
||||
g.set_color((0,0,0))
|
||||
g.set_fill_color((0,0,0))
|
||||
default_style.add_draw_style("TLG-solid",g)
|
||||
|
||||
g = BaseDoc.GraphicsStyle()
|
||||
g.set_line_width(0.5)
|
||||
g.set_color((0,0,0))
|
||||
g.set_fill_color((255,255,255))
|
||||
default_style.add_draw_style("open",g)
|
||||
|
||||
g = BaseDoc.GraphicsStyle()
|
||||
g.set_line_width(0.5)
|
||||
g.set_line_style(BaseDoc.DASHED)
|
||||
g.set_color((0,0,0))
|
||||
default_style.add_draw_style("TLG-grid",g)
|
||||
|
||||
g = BaseDoc.GraphicsStyle()
|
||||
g.set_paragraph_style("TLG-Name")
|
||||
g.set_color((0,0,0))
|
||||
g.set_fill_color((255,255,255))
|
||||
g.set_line_width(0)
|
||||
default_style.add_draw_style("TLG-text",g)
|
||||
|
||||
g = BaseDoc.GraphicsStyle()
|
||||
g.set_paragraph_style("TLG-Title")
|
||||
g.set_color((0,0,0))
|
||||
g.set_fill_color((255,255,255))
|
||||
g.set_line_width(0)
|
||||
default_style.add_draw_style("TLG-title",g)
|
||||
|
||||
g = BaseDoc.GraphicsStyle()
|
||||
g.set_paragraph_style("TLG-Label")
|
||||
g.set_color((0,0,0))
|
||||
g.set_fill_color((255,255,255))
|
||||
g.set_line_width(0)
|
||||
default_style.add_draw_style("TLG-label",g)
|
||||
|
||||
#------------------------------------------------------------------------
|
||||
#
|
||||
#
|
||||
#
|
||||
#------------------------------------------------------------------------
|
||||
pmgr = PluginManager.get_instance()
|
||||
pmgr.register_report(
|
||||
name = 'timeline',
|
||||
category = CATEGORY_DRAW,
|
||||
report_class = TimeLine,
|
||||
options_class = TimeLineOptions,
|
||||
modes = PluginManager.REPORT_MODE_GUI | \
|
||||
PluginManager.REPORT_MODE_BKI | \
|
||||
PluginManager.REPORT_MODE_CLI,
|
||||
translated_name = _("Timeline Chart"),
|
||||
status = _("Stable"),
|
||||
author_name = "Donald N. Allingham",
|
||||
author_email = "don@gramps-project.org",
|
||||
description = _("Produces a timeline chart.")
|
||||
)
|
||||
Reference in New Issue
Block a user