7b47dc61ad
svn: r11633
499 lines
24 KiB
Python
499 lines
24 KiB
Python
#
|
|
# Gramps - a GTK+/GNOME based genealogy program
|
|
#
|
|
# Copyright (C) 2007 Donald N. Allingham
|
|
# Copyright (C) 2008 Brian 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$
|
|
|
|
"Calculate Estimated Dates"
|
|
|
|
#------------------------------------------------------------------------
|
|
#
|
|
# python modules
|
|
#
|
|
#------------------------------------------------------------------------
|
|
from gettext import gettext as _
|
|
import time
|
|
|
|
#------------------------------------------------------------------------
|
|
#
|
|
# GRAMPS modules
|
|
#
|
|
#------------------------------------------------------------------------
|
|
from PluginUtils import Tool, PluginWindows, MenuToolOptions
|
|
from gen.plug import PluginManager
|
|
from gen.plug.menu import BooleanOption, NumberOption, StringOption, \
|
|
FilterOption, PersonOption
|
|
import gen.lib
|
|
import Config
|
|
from BasicUtils import name_displayer
|
|
import Errors
|
|
from ReportBase import ReportUtils
|
|
|
|
#------------------------------------------------------------------------
|
|
#
|
|
# Tool Classes
|
|
#
|
|
#------------------------------------------------------------------------
|
|
class CalcEstDateOptions(MenuToolOptions):
|
|
""" Calculate Estimated Date options """
|
|
def __init__(self, name, person_id=None, dbstate=None):
|
|
self.__db = dbstate.get_database()
|
|
MenuToolOptions.__init__(self, name, person_id, dbstate)
|
|
|
|
def add_menu_options(self, menu):
|
|
|
|
""" Add the options """
|
|
category_name = _("Options")
|
|
|
|
self.__filter = FilterOption(_("Filter"), 0)
|
|
self.__filter.set_help(_("Select filter to restrict people"))
|
|
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()
|
|
|
|
source_text = StringOption(_("Source text"),
|
|
_("Calculated Date Estimates"))
|
|
source_text.set_help(_("Source to remove and/or add"))
|
|
menu.add_option(category_name, "source_text", source_text)
|
|
|
|
remove = BooleanOption(_("Remove previously added dates"), True)
|
|
remove.set_help(_("Remove"))
|
|
menu.add_option(category_name, "remove", remove)
|
|
|
|
birth = BooleanOption(_("Add estimated birth dates"), True)
|
|
birth.set_help(_("Add"))
|
|
menu.add_option(category_name, "add_birth", birth)
|
|
|
|
death = BooleanOption(_("Add estimated death dates"), True)
|
|
death.set_help(_("Add estimated death dates"))
|
|
menu.add_option(category_name, "add_death", death)
|
|
|
|
display_details = BooleanOption(_("Display detailed results"), False)
|
|
display_details.set_help(_("Show details for every date entered"))
|
|
menu.add_option(category_name, "display_details", display_details)
|
|
|
|
# -----------------------------------------------------
|
|
category_name = _("Config")
|
|
num = NumberOption(_("Maximum age"),
|
|
Config.get(Config.MAX_AGE_PROB_ALIVE),
|
|
0, 200)
|
|
num.set_help(_("Maximum age that one can live to"))
|
|
menu.add_option(category_name, "MAX_AGE_PROB_ALIVE", num)
|
|
|
|
num = NumberOption(_("Maximum sibling age difference"),
|
|
Config.get(Config.MAX_SIB_AGE_DIFF),
|
|
0, 200)
|
|
num.set_help(_("Maximum age difference between siblings"))
|
|
menu.add_option(category_name, "MAX_SIB_AGE_DIFF", num)
|
|
|
|
num = NumberOption(_("Minimum years between generations"),
|
|
Config.get(Config.MIN_GENERATION_YEARS),
|
|
0, 200)
|
|
num.set_help(_("Minimum years between two generations"))
|
|
menu.add_option(category_name, "MIN_GENERATION_YEARS", num)
|
|
|
|
num = NumberOption(_("Average years between generations"),
|
|
Config.get(Config.AVG_GENERATION_GAP),
|
|
0, 200)
|
|
num.set_help(_("Average years between two generations"))
|
|
menu.add_option(category_name, "AVG_GENERATION_GAP", num)
|
|
|
|
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 0, 2, 3, 4 and 5 rely on the center person
|
|
self.__pid.set_available(True)
|
|
else:
|
|
# The rest don't
|
|
self.__pid.set_available(False)
|
|
|
|
|
|
class CalcToolManagedWindow(PluginWindows.ToolManagedWindowBatch):
|
|
|
|
def get_title(self):
|
|
return _("Calculate Estimated Dates")
|
|
|
|
def initial_frame(self):
|
|
return _("Options")
|
|
|
|
def run(self):
|
|
self.add_results_frame(_("Results"))
|
|
self.results_write(_("Processing...\n"))
|
|
self.trans = self.db.transaction_begin("",batch=True)
|
|
self.db.disable_signals()
|
|
self.filter_option = self.options.menu.get_option_by_name('filter')
|
|
self.filter = self.filter_option.get_filter() # the actual filter
|
|
people = self.filter.apply(self.db,
|
|
self.db.get_person_handles(sort_handles=False))
|
|
source_text = self.options.handler.options_dict['source_text']
|
|
add_birth = self.options.handler.options_dict['add_birth']
|
|
add_death = self.options.handler.options_dict['add_death']
|
|
remove_old = self.options.handler.options_dict['remove']
|
|
display_details = self.options.handler.options_dict['display_details']
|
|
|
|
self.MIN_GENERATION_YEARS = self.options.handler.options_dict['MIN_GENERATION_YEARS']
|
|
self.MAX_SIB_AGE_DIFF = self.options.handler.options_dict['MAX_SIB_AGE_DIFF']
|
|
self.MAX_AGE_PROB_ALIVE = self.options.handler.options_dict['MAX_AGE_PROB_ALIVE']
|
|
self.AVG_GENERATION_GAP = self.options.handler.options_dict['AVG_GENERATION_GAP']
|
|
if remove_old:
|
|
self.results_write(_("Replacing...\n"))
|
|
self.progress.set_pass((_("Removing '%s'...") % source_text),
|
|
len(people))
|
|
for person_handle in people:
|
|
self.progress.step()
|
|
pupdate = 0
|
|
person = self.db.get_person_from_handle(person_handle)
|
|
birth_ref = person.get_birth_ref()
|
|
if birth_ref:
|
|
birth = self.db.get_event_from_handle(birth_ref.ref)
|
|
source_list = birth.get_source_references()
|
|
for source_ref in source_list:
|
|
#print "birth handle:", source_ref
|
|
source = self.db.get_source_from_handle(source_ref.ref)
|
|
if source:
|
|
#print "birth source:", source, source.get_title()
|
|
if source.get_title() == source_text:
|
|
person.set_birth_ref(None)
|
|
person.remove_handle_references('Event',[birth_ref.ref])
|
|
self.db.remove_event(birth_ref.ref, self.trans)
|
|
self.db.commit_source(source, self.trans)
|
|
pupdate = 1
|
|
break
|
|
death_ref = person.get_death_ref()
|
|
if death_ref:
|
|
death = self.db.get_event_from_handle(death_ref.ref)
|
|
source_list = death.get_source_references()
|
|
for source_ref in source_list:
|
|
#print "death handle:", source_ref
|
|
source = self.db.get_source_from_handle(source_ref.ref)
|
|
if source:
|
|
#print "death source:", source, source.get_title()
|
|
if source.get_title() == source_text:
|
|
person.set_death_ref(None)
|
|
person.remove_handle_references('Event',[death_ref.ref])
|
|
self.db.remove_event(death_ref.ref, self.trans)
|
|
self.db.commit_source(source, self.trans)
|
|
pupdate = 1
|
|
break
|
|
if pupdate == 1:
|
|
self.db.commit_person(person, self.trans)
|
|
if add_birth or add_death:
|
|
self.results_write(_("Calculating...\n"))
|
|
self.progress.set_pass(_('Calculating estimated dates...'),
|
|
len(people))
|
|
source = self.get_or_create_source(source_text)
|
|
for person_handle in people:
|
|
self.progress.step()
|
|
added_birth, added_death = 0, 0
|
|
person = self.db.get_person_from_handle(person_handle)
|
|
birth_ref = person.get_birth_ref()
|
|
death_ref = person.get_death_ref()
|
|
#print birth_ref, death_ref
|
|
date1, date2 = self.calc_estimates(person, birth_ref, death_ref)
|
|
#print date1, date2
|
|
if not birth_ref and add_birth and date1:
|
|
#print "added birth"
|
|
birth = self.create_event(_("Estimated birth date"),
|
|
gen.lib.EventType.BIRTH,
|
|
date1, source)
|
|
event_ref = gen.lib.EventRef()
|
|
event_ref.set_reference_handle(birth.get_handle())
|
|
person.set_birth_ref(event_ref)
|
|
self.db.commit_person(person, self.trans)
|
|
added_birth = 1
|
|
if not death_ref and add_death and date2:
|
|
current_date = gen.lib.Date()
|
|
current_date.set_yr_mon_day(*time.localtime(time.time())[0:3])
|
|
if current_date.match( date2, "<<"):
|
|
# don't add events in the future!
|
|
pass # FIXME: sometimes adds one in future?
|
|
else:
|
|
#print "added death"
|
|
death = self.create_event(_("Estimated death date"),
|
|
gen.lib.EventType.DEATH,
|
|
date2, source)
|
|
event_ref = gen.lib.EventRef()
|
|
event_ref.set_reference_handle(death.get_handle())
|
|
person.set_death_ref(event_ref)
|
|
self.db.commit_person(person, self.trans)
|
|
added_death = 1
|
|
if (added_birth or added_death) and display_details:
|
|
self.results_write_link(name_displayer.display(person),
|
|
person, person_handle)
|
|
if added_birth:
|
|
self.results_write(_(" added birth on %s") % date1)
|
|
if added_death:
|
|
self.results_write(_(" added death on %s") % date2)
|
|
self.results_write("\n")
|
|
self.db.transaction_commit(self.trans, _("Calculate date estimates"))
|
|
self.db.enable_signals()
|
|
self.db.request_rebuild()
|
|
self.results_write(_("Done!\n"))
|
|
|
|
def get_or_create_source(self, source_text):
|
|
source_list = self.db.get_source_handles()
|
|
for source_handle in source_list:
|
|
source = self.db.get_source_from_handle(source_handle)
|
|
if source.get_title() == source_text:
|
|
return source
|
|
source = gen.lib.Source()
|
|
source.set_title(source_text)
|
|
self.db.add_source(source, self.trans)
|
|
return source
|
|
|
|
def create_event(self, description=_("Estimated date"),
|
|
type=None, date=None, source=None):
|
|
event = gen.lib.Event()
|
|
event.set_description(description)
|
|
if type:
|
|
event.set_type(gen.lib.EventType(type))
|
|
if date:
|
|
date.set_modifier(gen.lib.Date.MOD_ABOUT)
|
|
date.set_quality(gen.lib.Date.QUAL_ESTIMATED)
|
|
date.set_yr_mon_day(date.get_year(), 0, 0)
|
|
event.set_date_object(date)
|
|
if source:
|
|
sref = gen.lib.SourceRef()
|
|
sref.set_reference_handle(source.get_handle())
|
|
event.add_source_reference(sref)
|
|
self.db.commit_source(source, self.trans)
|
|
self.db.add_event(event, self.trans)
|
|
return event
|
|
|
|
def calc_estimates(self, person, birth_ref, death_ref):
|
|
death_date = None
|
|
birth_date = None
|
|
# If the recorded death year is before current year then
|
|
# things are simple.
|
|
if death_ref and death_ref.get_role() == gen.lib.EventRoleType.PRIMARY:
|
|
death = self.db.get_event_from_handle(death_ref.ref)
|
|
if death.get_date_object().get_start_date() != gen.lib.Date.EMPTY:
|
|
death_date = death.get_date_object()
|
|
|
|
# Look for Cause Of Death, Burial or Cremation events.
|
|
# These are fairly good indications that someone's not alive.
|
|
for ev_ref in person.get_primary_event_ref_list():
|
|
ev = self.db.get_event_from_handle(ev_ref.ref)
|
|
if ev and ev.type in [gen.lib.EventType.CAUSE_DEATH,
|
|
gen.lib.EventType.BURIAL,
|
|
gen.lib.EventType.CREMATION]:
|
|
if not death_date:
|
|
death_date = ev.get_date_object()
|
|
|
|
# If they were born within 100 years before current year then
|
|
# assume they are alive (we already know they are not dead).
|
|
if birth_ref and birth_ref.get_role() == gen.lib.EventRoleType.PRIMARY:
|
|
birth = self.db.get_event_from_handle(birth_ref.ref)
|
|
if birth.get_date_object().get_start_date() != gen.lib.Date.EMPTY:
|
|
if not birth_date:
|
|
birth_date = birth.get_date_object()
|
|
|
|
#print " calculating...", birth_date, death_date
|
|
|
|
if not birth_date and death_date:
|
|
# person died more than MAX after current year
|
|
birth_date = death_date.copy_offset_ymd(year=-self.MAX_AGE_PROB_ALIVE)
|
|
|
|
if not death_date and birth_date:
|
|
# person died more than MAX after current year
|
|
death_date = birth_date.copy_offset_ymd(year=self.MAX_AGE_PROB_ALIVE)
|
|
|
|
if death_date and birth_date:
|
|
return (birth_date, death_date)
|
|
|
|
# Neither birth nor death events are available. Try looking
|
|
# at siblings. If a sibling was born more than 120 years past,
|
|
# or more than 20 future, then probably this person is
|
|
# not alive. If the sibling died more than 120 years
|
|
# past, or more than 120 years future, then probably not alive.
|
|
|
|
#print " searching family..."
|
|
|
|
family_list = person.get_parent_family_handle_list()
|
|
for family_handle in family_list:
|
|
family = self.db.get_family_from_handle(family_handle)
|
|
for child_ref in family.get_child_ref_list():
|
|
child_handle = child_ref.ref
|
|
child = self.db.get_person_from_handle(child_handle)
|
|
child_birth_ref = child.get_birth_ref()
|
|
if child_birth_ref:
|
|
child_birth = self.db.get_event_from_handle(child_birth_ref.ref)
|
|
dobj = child_birth.get_date_object()
|
|
if dobj.get_start_date() != gen.lib.Date.EMPTY:
|
|
# if sibling birth date too far away, then not alive:
|
|
year = dobj.get_year()
|
|
if year != 0:
|
|
# sibling birth date
|
|
return (gen.lib.Date().copy_ymd(year - self.MAX_SIB_AGE_DIFF),
|
|
gen.lib.Date().copy_ymd(year + self.MAX_SIB_AGE_DIFF + self.MAX_AGE_PROB_ALIVE))
|
|
child_death_ref = child.get_death_ref()
|
|
if child_death_ref:
|
|
child_death = self.db.get_event_from_handle(child_death_ref.ref)
|
|
dobj = child_death.get_date_object()
|
|
if dobj.get_start_date() != gen.lib.Date.EMPTY:
|
|
# if sibling death date too far away, then not alive:
|
|
year = dobj.get_year()
|
|
if year != 0:
|
|
# sibling death date
|
|
return (gen.lib.Date().copy_ymd(year - self.MAX_SIB_AGE_DIFF - self.MAX_AGE_PROB_ALIVE),
|
|
gen.lib.Date().copy_ymd(year + self.MAX_SIB_AGE_DIFF))
|
|
|
|
# Try looking for descendants that were born more than a lifespan
|
|
# ago.
|
|
|
|
def descendants_too_old (person, years):
|
|
for family_handle in person.get_family_handle_list():
|
|
family = self.db.get_family_from_handle(family_handle)
|
|
for child_ref in family.get_child_ref_list():
|
|
child_handle = child_ref.ref
|
|
child = self.db.get_person_from_handle(child_handle)
|
|
child_birth_ref = child.get_birth_ref()
|
|
if child_birth_ref:
|
|
child_birth = self.db.get_event_from_handle(child_birth_ref.ref)
|
|
dobj = child_birth.get_date_object()
|
|
if dobj.get_start_date() != gen.lib.Date.EMPTY:
|
|
d = gen.lib.Date(dobj)
|
|
val = d.get_start_date()
|
|
val = d.get_year() - years
|
|
d.set_year(val)
|
|
return (d, d.copy_offset_ymd(self.MAX_AGE_PROB_ALIVE))
|
|
child_death_ref = child.get_death_ref()
|
|
if child_death_ref:
|
|
child_death = self.db.get_event_from_handle(child_death_ref.ref)
|
|
dobj = child_death.get_date_object()
|
|
if dobj.get_start_date() != gen.lib.Date.EMPTY:
|
|
return (dobj.copy_offset_ymd(- self.MIN_GENERATION_YEARS),
|
|
dobj.copy_offset_ymd(- self.MIN_GENERATION_YEARS + self.MAX_AGE_PROB_ALIVE))
|
|
date1, date2 = descendants_too_old (child, years + self.MIN_GENERATION_YEARS)
|
|
if date1 and date2:
|
|
return date1, date2
|
|
return (None, None)
|
|
|
|
# If there are descendants that are too old for the person to have
|
|
# been alive in the current year then they must be dead.
|
|
|
|
date1, date2 = None, None
|
|
try:
|
|
date1, date2 = descendants_too_old(person, self.MIN_GENERATION_YEARS)
|
|
except RuntimeError:
|
|
raise Errors.DatabaseError(
|
|
_("Database error: %s is defined as his or her own ancestor") %
|
|
name_displayer.display(person))
|
|
|
|
if date1 and date2:
|
|
return (date1, date2)
|
|
|
|
def ancestors_too_old(person, year):
|
|
family_handle = person.get_main_parents_family_handle()
|
|
if family_handle:
|
|
family = self.db.get_family_from_handle(family_handle)
|
|
father_handle = family.get_father_handle()
|
|
if father_handle:
|
|
father = self.db.get_person_from_handle(father_handle)
|
|
father_birth_ref = father.get_birth_ref()
|
|
if father_birth_ref and father_birth_ref.get_role() == gen.lib.EventRoleType.PRIMARY:
|
|
father_birth = self.db.get_event_from_handle(
|
|
father_birth_ref.ref)
|
|
dobj = father_birth.get_date_object()
|
|
if dobj.get_start_date() != gen.lib.Date.EMPTY:
|
|
return (dobj.copy_offset_ymd(- year),
|
|
dobj.copy_offset_ymd(- year + self.MAX_AGE_PROB_ALIVE))
|
|
father_death_ref = father.get_death_ref()
|
|
if father_death_ref and father_death_ref.get_role() == gen.lib.EventRoleType.PRIMARY:
|
|
father_death = self.db.get_event_from_handle(
|
|
father_death_ref.ref)
|
|
dobj = father_death.get_date_object()
|
|
if dobj.get_start_date() != gen.lib.Date.EMPTY:
|
|
return (dobj.copy_offset_ymd(- year - self.MAX_AGE_PROB_ALIVE),
|
|
dobj.copy_offset_ymd(- year - self.MAX_AGE_PROB_ALIVE + self.MAX_AGE_PROB_ALIVE))
|
|
date1, date2 = ancestors_too_old (father, year - self.AVG_GENERATION_GAP)
|
|
if date1 and date2:
|
|
return date1, date2
|
|
mother_handle = family.get_mother_handle()
|
|
if mother_handle:
|
|
mother = self.db.get_person_from_handle(mother_handle)
|
|
mother_birth_ref = mother.get_birth_ref()
|
|
if mother_birth_ref and mother_birth_ref.get_role() == gen.lib.EventRoleType.PRIMARY:
|
|
mother_birth = self.db.get_event_from_handle(mother_birth_ref.ref)
|
|
dobj = mother_birth.get_date_object()
|
|
if dobj.get_start_date() != gen.lib.Date.EMPTY:
|
|
return (dobj.copy_offset_ymd(- year),
|
|
dobj.copy_offset_ymd(- year + self.MAX_AGE_PROB_ALIVE))
|
|
mother_death_ref = mother.get_death_ref()
|
|
if mother_death_ref and mother_death_ref.get_role() == gen.lib.EventRoleType.PRIMARY:
|
|
mother_death = self.db.get_event_from_handle(
|
|
mother_death_ref.ref)
|
|
dobj = mother_death.get_date_object()
|
|
if dobj.get_start_date() != gen.lib.Date.EMPTY:
|
|
return (dobj.copy_offset_ymd(- year - self.MAX_AGE_PROB_ALIVE),
|
|
dobj.copy_offset_ymd(- year - self.MAX_AGE_PROB_ALIVE + self.MAX_AGE_PROB_ALIVE))
|
|
date1, date2 = ancestors_too_old (mother, year - self.AVG_GENERATION_GAP)
|
|
if date1 and date2:
|
|
return (date1, date2)
|
|
return (None, None)
|
|
|
|
# If there are ancestors that would be too old in the current year
|
|
# then assume our person must be dead too.
|
|
date1, date2 = ancestors_too_old (person, - self.MIN_GENERATION_YEARS)
|
|
if date1 and date2:
|
|
return (date1, date2)
|
|
#print " FAIL"
|
|
# If we can't find any reason to believe that they are dead we
|
|
# must assume they are alive.
|
|
return (None, None)
|
|
|
|
#-------------------------------------------------------------------------
|
|
#
|
|
# Register the tool
|
|
#
|
|
#-------------------------------------------------------------------------
|
|
pmgr = PluginManager.get_instance()
|
|
pmgr.register_tool(
|
|
name = 'calculateestimateddates',
|
|
category = Tool.TOOL_DBPROC,
|
|
tool_class = CalcToolManagedWindow,
|
|
options_class = CalcEstDateOptions,
|
|
modes = PluginManager.TOOL_MODE_GUI,
|
|
translated_name = _("Calculate Estimated Dates"),
|
|
status = _("Beta"),
|
|
author_name = "Douglas S. Blank",
|
|
author_email = "dblank@cs.brynmawr.edu",
|
|
description= _("Calculates estimated dates for birth and death.")
|
|
)
|