1# 2# Gramps - a GTK+/GNOME based genealogy program 3# 4# Copyright (C) 2007-2008 Stephane Charette 5# Copyright (C) 2007-2008 Brian G. Matherly 6# Copyright (C) 2009-2010 Gary Burton 7# Contribution 2009 by Bob Ham <rah@bash.sh> 8# Copyright (C) 2010 Jakim Friant 9# Copyright (C) 2011-2014 Paul Franklin 10# 11# This program is free software; you can redistribute it and/or modify 12# it under the terms of the GNU General Public License as published by 13# the Free Software Foundation; either version 2 of the License, or 14# (at your option) any later version. 15# 16# This program is distributed in the hope that it will be useful, 17# but WITHOUT ANY WARRANTY; without even the implied warranty of 18# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 19# GNU General Public License for more details. 20# 21# You should have received a copy of the GNU General Public License 22# along with this program; if not, write to the Free Software 23# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 24# 25 26""" 27Family Lines, a Graphviz-based plugin for Gramps. 28""" 29 30#------------------------------------------------------------------------ 31# 32# python modules 33# 34#------------------------------------------------------------------------ 35from functools import partial 36import html 37 38#------------------------------------------------------------------------ 39# 40# Set up logging 41# 42#------------------------------------------------------------------------ 43import logging 44LOG = logging.getLogger(".FamilyLines") 45 46#------------------------------------------------------------------------ 47# 48# Gramps module 49# 50#------------------------------------------------------------------------ 51from gramps.gen.const import GRAMPS_LOCALE as glocale 52_ = glocale.translation.gettext 53from gramps.gen.lib import EventRoleType, EventType, Person, PlaceType, Date 54from gramps.gen.utils.file import media_path_full 55from gramps.gen.utils.thumbnails import (get_thumbnail_path, SIZE_NORMAL, 56 SIZE_LARGE) 57from gramps.gen.plug.report import Report 58from gramps.gen.plug.report import utils 59from gramps.gen.plug.report import MenuReportOptions 60from gramps.gen.plug.report import stdoptions 61from gramps.gen.plug.menu import (NumberOption, ColorOption, BooleanOption, 62 EnumeratedListOption, PersonListOption, 63 SurnameColorOption) 64from gramps.gen.utils.db import get_birth_or_fallback, get_death_or_fallback 65from gramps.gen.proxy import CacheProxyDb 66from gramps.gen.errors import ReportError 67from gramps.gen.display.place import displayer as _pd 68 69#------------------------------------------------------------------------ 70# 71# Constant options items 72# 73#------------------------------------------------------------------------ 74_COLORS = [{'name' : _("B&W outline"), 'value' : "outline"}, 75 {'name' : _("Colored outline"), 'value' : "colored"}, 76 {'name' : _("Color fill"), 'value' : "filled"}] 77 78_ARROWS = [ { 'name' : _("Descendants <- Ancestors"), 'value' : 'd' }, 79 { 'name' : _("Descendants -> Ancestors"), 'value' : 'a' }, 80 { 'name' : _("Descendants <-> Ancestors"), 'value' : 'da' }, 81 { 'name' : _("Descendants - Ancestors"), 'value' : '' }] 82 83_CORNERS = [ { 'name' : _("None"), 'value' : '' }, 84 { 'name' : _("Female"), 'value' : 'f' }, 85 { 'name' : _("Male"), 'value' : 'm' }, 86 { 'name' : _("Both"), 'value' : 'fm' }] 87 88#------------------------------------------------------------------------ 89# 90# A quick overview of the classes we'll be using: 91# 92# class FamilyLinesOptions(MenuReportOptions) 93# - this class is created when the report dialog comes up 94# - all configuration controls for the report are created here 95# 96# class FamilyLinesReport(Report) 97# - this class is created only after the user clicks on "OK" 98# - the actual report generation is done by this class 99# 100#------------------------------------------------------------------------ 101 102class FamilyLinesOptions(MenuReportOptions): 103 """ 104 Defines all of the controls necessary 105 to configure the FamilyLines report. 106 """ 107 def __init__(self, name, dbase): 108 self.limit_parents = None 109 self.max_parents = None 110 self.limit_children = None 111 self.max_children = None 112 self.include_images = None 113 self.image_location = None 114 self.justyears = None 115 self.include_dates = None 116 MenuReportOptions.__init__(self, name, dbase) 117 118 def add_menu_options(self, menu): 119 120 # --------------------- 121 category_name = _('Report Options') 122 add_option = partial(menu.add_option, category_name) 123 # --------------------- 124 125 followpar = BooleanOption(_('Follow parents to determine ' 126 '"family lines"'), True) 127 followpar.set_help(_('Parents and their ancestors will be ' 128 'considered when determining "family lines".')) 129 add_option('followpar', followpar) 130 131 followchild = BooleanOption(_('Follow children to determine ' 132 '"family lines"'), True) 133 followchild.set_help(_('Children will be considered when ' 134 'determining "family lines".')) 135 add_option('followchild', followchild) 136 137 remove_extra_people = BooleanOption(_('Try to remove extra ' 138 'people and families'), True) 139 remove_extra_people.set_help(_('People and families not directly ' 140 'related to people of interest will ' 141 'be removed when determining ' 142 '"family lines".')) 143 add_option('removeextra', remove_extra_people) 144 145 arrow = EnumeratedListOption(_("Arrowhead direction"), 'd') 146 for i in range( 0, len(_ARROWS) ): 147 arrow.add_item(_ARROWS[i]["value"], _ARROWS[i]["name"]) 148 arrow.set_help(_("Choose the direction that the arrows point.")) 149 add_option("arrow", arrow) 150 151 color = EnumeratedListOption(_("Graph coloring"), "filled") 152 for i in range(len(_COLORS)): 153 color.add_item(_COLORS[i]["value"], _COLORS[i]["name"]) 154 color.set_help(_("Males will be shown with blue, females " 155 "with red, unless otherwise set above for filled. " 156 "If the sex of an individual " 157 "is unknown it will be shown with gray.")) 158 add_option("color", color) 159 160 roundedcorners = EnumeratedListOption(_("Rounded corners"), '') 161 for i in range( 0, len(_CORNERS) ): 162 roundedcorners.add_item(_CORNERS[i]["value"], _CORNERS[i]["name"]) 163 roundedcorners.set_help(_("Use rounded corners e.g. to differentiate " 164 "between women and men.")) 165 add_option("useroundedcorners", roundedcorners) 166 167 stdoptions.add_gramps_id_option(menu, category_name, ownline=True) 168 169 # --------------------- 170 category_name = _('Report Options (2)') 171 add_option = partial(menu.add_option, category_name) 172 # --------------------- 173 174 stdoptions.add_name_format_option(menu, category_name) 175 176 stdoptions.add_private_data_option(menu, category_name, default=False) 177 178 stdoptions.add_living_people_option(menu, category_name) 179 180 locale_opt = stdoptions.add_localization_option(menu, category_name) 181 182 stdoptions.add_date_format_option(menu, category_name, locale_opt) 183 184 # -------------------------------- 185 add_option = partial(menu.add_option, _('People of Interest')) 186 # -------------------------------- 187 188 person_list = PersonListOption(_('People of interest')) 189 person_list.set_help(_('People of interest are used as a starting ' 190 'point when determining "family lines".')) 191 add_option('gidlist', person_list) 192 193 self.limit_parents = BooleanOption(_('Limit the number of ancestors'), 194 False) 195 self.limit_parents.set_help(_('Whether to ' 196 'limit the number of ancestors.')) 197 add_option('limitparents', self.limit_parents) 198 self.limit_parents.connect('value-changed', self.limit_changed) 199 200 self.max_parents = NumberOption('', 50, 10, 9999) 201 self.max_parents.set_help(_('The maximum number ' 202 'of ancestors to include.')) 203 add_option('maxparents', self.max_parents) 204 205 self.limit_children = BooleanOption(_('Limit the number ' 206 'of descendants'), 207 False) 208 self.limit_children.set_help(_('Whether to ' 209 'limit the number of descendants.')) 210 add_option('limitchildren', self.limit_children) 211 self.limit_children.connect('value-changed', self.limit_changed) 212 213 self.max_children = NumberOption('', 50, 10, 9999) 214 self.max_children.set_help(_('The maximum number ' 215 'of descendants to include.')) 216 add_option('maxchildren', self.max_children) 217 218 # -------------------- 219 category_name = _('Include') 220 add_option = partial(menu.add_option, category_name) 221 # -------------------- 222 223 self.include_dates = BooleanOption(_('Include dates'), True) 224 self.include_dates.set_help(_('Whether to include dates for people ' 225 'and families.')) 226 add_option('incdates', self.include_dates) 227 self.include_dates.connect('value-changed', self.include_dates_changed) 228 229 self.justyears = BooleanOption(_("Limit dates to years only"), False) 230 self.justyears.set_help(_("Prints just dates' year, neither " 231 "month or day nor date approximation " 232 "or interval are shown.")) 233 add_option("justyears", self.justyears) 234 235 include_places = BooleanOption(_('Include places'), True) 236 include_places.set_help(_('Whether to include placenames for people ' 237 'and families.')) 238 add_option('incplaces', include_places) 239 240 include_num_children = BooleanOption(_('Include the number of ' 241 'children'), True) 242 include_num_children.set_help(_('Whether to include the number of ' 243 'children for families with more ' 244 'than 1 child.')) 245 add_option('incchildcnt', include_num_children) 246 247 self.include_images = BooleanOption(_('Include ' 248 'thumbnail images of people'), 249 True) 250 self.include_images.set_help(_('Whether to ' 251 'include thumbnail images of people.')) 252 add_option('incimages', self.include_images) 253 self.include_images.connect('value-changed', self.images_changed) 254 255 self.image_location = EnumeratedListOption(_('Thumbnail location'), 0) 256 self.image_location.add_item(0, _('Above the name')) 257 self.image_location.add_item(1, _('Beside the name')) 258 self.image_location.set_help(_('Where the thumbnail image ' 259 'should appear relative to the name')) 260 add_option('imageonside', self.image_location) 261 262 self.image_size = EnumeratedListOption(_('Thumbnail size'), SIZE_NORMAL) 263 self.image_size.add_item(SIZE_NORMAL, _('Normal')) 264 self.image_size.add_item(SIZE_LARGE, _('Large')) 265 self.image_size.set_help(_('Size of the thumbnail image')) 266 add_option('imagesize', self.image_size) 267 268 # ---------------------------- 269 add_option = partial(menu.add_option, _('Family Colors')) 270 # ---------------------------- 271 272 surname_color = SurnameColorOption(_('Family colors')) 273 surname_color.set_help(_('Colors to use for various family lines.')) 274 add_option('surnamecolors', surname_color) 275 276 # ------------------------- 277 add_option = partial(menu.add_option, _('Individuals')) 278 # ------------------------- 279 280 color_males = ColorOption(_('Males'), '#e0e0ff') 281 color_males.set_help(_('The color to use to display men.')) 282 add_option('colormales', color_males) 283 284 color_females = ColorOption(_('Females'), '#ffe0e0') 285 color_females.set_help(_('The color to use to display women.')) 286 add_option('colorfemales', color_females) 287 288 color_unknown = ColorOption(_('Unknown'), '#e0e0e0') 289 color_unknown.set_help(_('The color to use ' 290 'when the gender is unknown.')) 291 add_option('colorunknown', color_unknown) 292 293 color_family = ColorOption(_('Families'), '#ffffe0') 294 color_family.set_help(_('The color to use to display families.')) 295 add_option('colorfamilies', color_family) 296 297 self.limit_changed() 298 self.images_changed() 299 300 def limit_changed(self): 301 """ 302 Handle the change of limiting parents and children. 303 """ 304 self.max_parents.set_available(self.limit_parents.get_value()) 305 self.max_children.set_available(self.limit_children.get_value()) 306 307 def images_changed(self): 308 """ 309 Handle the change of including images. 310 """ 311 self.image_location.set_available(self.include_images.get_value()) 312 self.image_size.set_available(self.include_images.get_value()) 313 314 def include_dates_changed(self): 315 """ 316 Enable/disable menu items if dates are required 317 """ 318 if self.include_dates.get_value(): 319 self.justyears.set_available(True) 320 else: 321 self.justyears.set_available(False) 322 323#------------------------------------------------------------------------ 324# 325# FamilyLinesReport -- created once the user presses 'OK' 326# 327#------------------------------------------------------------------------ 328class FamilyLinesReport(Report): 329 """ FamilyLines report """ 330 331 def __init__(self, database, options, user): 332 """ 333 Create FamilyLinesReport object that eventually produces the report. 334 335 The arguments are: 336 337 database - the Gramps database instance 338 options - instance of the FamilyLinesOptions class for this report 339 user - a gen.user.User() instance 340 name_format - Preferred format to display names 341 incl_private - Whether to include private data 342 inc_id - Whether to include IDs. 343 living_people - How to handle living people 344 years_past_death - Consider as living this many years after death 345 """ 346 Report.__init__(self, database, options, user) 347 348 menu = options.menu 349 get_option_by_name = menu.get_option_by_name 350 get_value = lambda name: get_option_by_name(name).get_value() 351 352 self.set_locale(menu.get_option_by_name('trans').get_value()) 353 354 stdoptions.run_date_format_option(self, menu) 355 356 stdoptions.run_private_data_option(self, menu) 357 stdoptions.run_living_people_option(self, menu, self._locale) 358 self.database = CacheProxyDb(self.database) 359 self._db = self.database 360 361 # initialize several convenient variables 362 self._people = set() # handle of people we need in the report 363 self._families = set() # handle of families we need in the report 364 self._deleted_people = 0 365 self._deleted_families = 0 366 self._user = user 367 368 self._followpar = get_value('followpar') 369 self._followchild = get_value('followchild') 370 self._removeextra = get_value('removeextra') 371 self._gidlist = get_value('gidlist') 372 self._colormales = get_value('colormales') 373 self._colorfemales = get_value('colorfemales') 374 self._colorunknown = get_value('colorunknown') 375 self._colorfamilies = get_value('colorfamilies') 376 self._limitparents = get_value('limitparents') 377 self._maxparents = get_value('maxparents') 378 self._limitchildren = get_value('limitchildren') 379 self._maxchildren = get_value('maxchildren') 380 self._incimages = get_value('incimages') 381 self._imageonside = get_value('imageonside') 382 self._imagesize = get_value('imagesize') 383 self._useroundedcorners = get_value('useroundedcorners') 384 self._usesubgraphs = get_value('usesubgraphs') 385 self._incdates = get_value('incdates') 386 self._just_years = get_value('justyears') 387 self._incplaces = get_value('incplaces') 388 self._incchildcount = get_value('incchildcnt') 389 self.includeid = get_value('inc_id') 390 391 arrow_str = get_value('arrow') 392 if 'd' in arrow_str: 393 self._arrowheadstyle = 'normal' 394 else: 395 self._arrowheadstyle = 'none' 396 if 'a' in arrow_str: 397 self._arrowtailstyle = 'normal' 398 else: 399 self._arrowtailstyle = 'none' 400 401 # the gidlist is annoying for us to use since we always have to convert 402 # the GIDs to either Person or to handles, so we may as well convert the 403 # entire list right now and not have to deal with it ever again 404 self._interest_set = set() 405 if not self._gidlist: 406 raise ReportError(_('Empty report'), 407 _('You did not specify anybody')) 408 for gid in self._gidlist.split(): 409 person = self._db.get_person_from_gramps_id(gid) 410 if person: 411 #option can be from another family tree, so person can be None 412 self._interest_set.add(person.get_handle()) 413 414 stdoptions.run_name_format_option(self, menu) 415 416 # convert the 'surnamecolors' string to a dictionary of names and colors 417 self._surnamecolors = {} 418 tmp = get_value('surnamecolors') 419 if tmp.find('\xb0') >= 0: 420 # new style delimiter (see bug report #2162) 421 tmp = tmp.split('\xb0') 422 else: 423 # old style delimiter 424 tmp = tmp.split(' ') 425 426 while len(tmp) > 1: 427 surname = tmp.pop(0).encode('iso-8859-1', 'xmlcharrefreplace') 428 colour = tmp.pop(0) 429 self._surnamecolors[surname] = colour 430 431 self._colorize = get_value('color') 432 433 def begin_report(self): 434 """ 435 Inherited method; called by report() in _ReportDialog.py 436 437 This is where we'll do all of the work of figuring out who 438 from the database is going to be output into the report 439 """ 440 441 # starting with the people of interest, we then add parents: 442 self._people.clear() 443 self._families.clear() 444 if self._followpar: 445 self.find_parents() 446 447 if self._removeextra: 448 self.remove_uninteresting_parents() 449 450 # ...and/or with the people of interest we add their children: 451 if self._followchild: 452 self.find_children() 453 # once we get here we have a full list of people 454 # and families that we need to generate a report 455 456 457 def write_report(self): 458 """ 459 Inherited method; called by report() in _ReportDialog.py 460 """ 461 462 # now that begin_report() has done the work, output what we've 463 # obtained into whatever file or format the user expects to use 464 465 self.doc.add_comment('# %s %d' % 466 (self._('Number of people in database:'), 467 self._db.get_number_of_people())) 468 self.doc.add_comment('# %s %d' % 469 (self._('Number of people of interest:'), 470 len(self._people))) 471 self.doc.add_comment('# %s %d' % 472 (self._('Number of families in database:'), 473 self._db.get_number_of_families())) 474 self.doc.add_comment('# %s %d' % 475 (self._('Number of families of interest:'), 476 len(self._families))) 477 if self._removeextra: 478 self.doc.add_comment('# %s %d' % 479 (self._('Additional people removed:'), 480 self._deleted_people)) 481 self.doc.add_comment('# %s %d' % 482 (self._('Additional families removed:'), 483 self._deleted_families)) 484 self.doc.add_comment('# %s' % 485 self._('Initial list of people of interest:')) 486 for handle in self._interest_set: 487 person = self._db.get_person_from_handle(handle) 488 gid = person.get_gramps_id() 489 name = person.get_primary_name().get_regular_name() 490 # translators: needed for Arabic, ignore othewise 491 id_n = self._("%(str1)s, %(str2)s") % {'str1':gid, 'str2':name} 492 self.doc.add_comment('# -> ' + id_n) 493 494 self.write_people() 495 self.write_families() 496 497 def find_parents(self): 498 """ find the parents """ 499 # we need to start with all of our "people of interest" 500 ancestors_not_yet_processed = set(self._interest_set) 501 502 # now we find all the immediate ancestors of our people of interest 503 504 while ancestors_not_yet_processed: 505 handle = ancestors_not_yet_processed.pop() 506 507 # One of 2 things can happen here: 508 # 1) we already know about this person and he/she is already 509 # in our list 510 # 2) this is someone new, and we need to remember him/her 511 # 512 # In the first case, there isn't anything else to do, so we simply 513 # go back to the top and pop the next person off the list. 514 # 515 # In the second case, we need to add this person to our list, and 516 # then go through all of the parents this person has to find more 517 # people of interest. 518 519 if handle not in self._people: 520 521 person = self._db.get_person_from_handle(handle) 522 523 # remember this person! 524 self._people.add(handle) 525 526 # see if a family exists between this person and someone else 527 # we have on our list of people we're going to output -- if 528 # there is a family, then remember it for when it comes time 529 # to link spouses together 530 for family_handle in person.get_family_handle_list(): 531 family = self._db.get_family_from_handle(family_handle) 532 if not family: 533 continue 534 spouse_handle = utils.find_spouse(person, family) 535 if spouse_handle: 536 if (spouse_handle in self._people or 537 spouse_handle in ancestors_not_yet_processed): 538 self._families.add(family_handle) 539 540 # if we have a limit on the number of people, and we've 541 # reached that limit, then don't attempt to find any 542 # more ancestors 543 if (self._limitparents and 544 (self._maxparents < 545 len(ancestors_not_yet_processed) + len(self._people))): 546 # get back to the top of the while loop so we can finish 547 # processing the people queued up in the "not yet 548 # processed" list 549 continue 550 551 # queue the parents of the person we're processing 552 for family_handle in person.get_parent_family_handle_list(): 553 family = self._db.get_family_from_handle(family_handle) 554 555 father_handle = family.get_father_handle() 556 if father_handle: 557 father = self._db.get_person_from_handle(father_handle) 558 if father: 559 ancestors_not_yet_processed.add(father_handle) 560 self._families.add(family_handle) 561 562 mother_handle = family.get_mother_handle() 563 if mother_handle: 564 mother = self._db.get_person_from_handle(mother_handle) 565 if mother: 566 ancestors_not_yet_processed.add(mother_handle) 567 self._families.add(family_handle) 568 569 def remove_uninteresting_parents(self): 570 """ remove any uninteresting parents """ 571 # start with all the people we've already identified 572 unprocessed_parents = set(self._people) 573 574 while len(unprocessed_parents) > 0: 575 handle = unprocessed_parents.pop() 576 person = self._db.get_person_from_handle(handle) 577 if not person: 578 continue 579 580 # There are a few things we're going to need, 581 # so look it all up right now; such as: 582 # - who is the child? 583 # - how many children? 584 # - parents? 585 # - spouse? 586 # - is a person of interest? 587 # - spouse of a person of interest? 588 # - same surname as a person of interest? 589 # - spouse has the same surname as a person of interest? 590 591 child_handle = None 592 child_count = 0 593 spouse_handle = None 594 spouse_count = 0 595 father_handle = None 596 mother_handle = None 597 spouse_father_handle = None 598 spouse_mother_handle = None 599 spouse_surname = "" 600 surname = person.get_primary_name().get_surname() 601 surname = surname.encode('iso-8859-1', 'xmlcharrefreplace') 602 603 # first we get the person's father and mother 604 for family_handle in person.get_parent_family_handle_list(): 605 family = self._db.get_family_from_handle(family_handle) 606 handle = family.get_father_handle() 607 if handle in self._people: 608 father_handle = handle 609 handle = family.get_mother_handle() 610 if handle in self._people: 611 mother_handle = handle 612 613 # now see how many spouses this person has 614 for family_handle in person.get_family_handle_list(): 615 family = self._db.get_family_from_handle(family_handle) 616 handle = utils.find_spouse(person, family) 617 if handle in self._people: 618 spouse_count += 1 619 spouse = self._db.get_person_from_handle(handle) 620 spouse_handle = handle 621 spouse_surname = spouse.get_primary_name().get_surname() 622 spouse_surname = spouse_surname.encode( 623 'iso-8859-1', 'xmlcharrefreplace') 624 625 # see if the spouse has parents 626 if not spouse_father_handle and not spouse_mother_handle: 627 for family_handle in \ 628 spouse.get_parent_family_handle_list(): 629 family = self._db.get_family_from_handle( 630 family_handle) 631 handle = family.get_father_handle() 632 if handle in self._people: 633 spouse_father_handle = handle 634 handle = family.get_mother_handle() 635 if handle in self._people: 636 spouse_mother_handle = handle 637 638 # get the number of children that we think might be interesting 639 for family_handle in person.get_family_handle_list(): 640 family = self._db.get_family_from_handle(family_handle) 641 for child_ref in family.get_child_ref_list(): 642 if child_ref.ref in self._people: 643 child_count += 1 644 child_handle = child_ref.ref 645 646 # we now have everything we need -- start looking for reasons 647 # why this is a person we need to keep in our list, and loop 648 # back to the top as soon as a reason is discovered 649 650 # if this person has many children of interest, then we 651 # automatically keep this person 652 if child_count > 1: 653 continue 654 655 # if this person has many spouses of interest, then we 656 # automatically keep this person 657 if spouse_count > 1: 658 continue 659 660 # if this person has parents, then we automatically keep 661 # this person 662 if father_handle is not None or mother_handle is not None: 663 continue 664 665 # if the spouse has parents, then we automatically keep 666 # this person 667 if (spouse_father_handle is not None or 668 spouse_mother_handle is not None): 669 continue 670 671 # if this is a person of interest, then we automatically keep 672 if person.get_handle() in self._interest_set: 673 continue 674 675 # if the spouse is a person of interest, then we keep 676 if spouse_handle in self._interest_set: 677 continue 678 679 # if the surname (or the spouse's surname) matches a person 680 # of interest, then we automatically keep this person 681 keep_this_person = False 682 for person_of_interest_handle in self._interest_set: 683 person_of_interest = self._db.get_person_from_handle( 684 person_of_interest_handle) 685 surname_of_interest = person_of_interest.get_primary_name() 686 surname_of_interest = surname_of_interest.get_surname().encode( 687 'iso-8859-1', 'xmlcharrefreplace') 688 if (surname_of_interest == surname or 689 surname_of_interest == spouse_surname): 690 keep_this_person = True 691 break 692 693 if keep_this_person: 694 continue 695 696 # if we have a special colour to use for this person, 697 # then we automatically keep this person 698 if surname in self._surnamecolors: 699 continue 700 701 # if we have a special colour to use for the spouse, 702 # then we automatically keep this person 703 if spouse_surname in self._surnamecolors: 704 continue 705 706 # took us a while, 707 # but if we get here then we can remove this person 708 self._deleted_people += 1 709 self._people.remove(person.get_handle()) 710 711 # we can also remove any families to which this person belonged 712 for family_handle in person.get_family_handle_list(): 713 if family_handle in self._families: 714 self._deleted_families += 1 715 self._families.remove(family_handle) 716 717 # if we have a spouse, then ensure we queue up the spouse 718 if spouse_handle: 719 if spouse_handle not in unprocessed_parents: 720 unprocessed_parents.add(spouse_handle) 721 722 # if we have a child, then ensure we queue up the child 723 if child_handle: 724 if child_handle not in unprocessed_parents: 725 unprocessed_parents.add(child_handle) 726 727 728 def find_children(self): 729 """ find any children """ 730 # we need to start with all of our "people of interest" 731 children_not_yet_processed = set(self._interest_set) 732 children_to_include = set() 733 734 # now we find all the children of our people of interest 735 736 while len(children_not_yet_processed) > 0: 737 handle = children_not_yet_processed.pop() 738 739 if handle not in children_to_include: 740 741 person = self._db.get_person_from_handle(handle) 742 743 # remember this person! 744 children_to_include.add(handle) 745 746 # if we have a limit on the number of people, and we've 747 # reached that limit, then don't attempt to find any 748 # more children 749 if (self._limitchildren and 750 (self._maxchildren < 751 len(children_not_yet_processed) + 752 len(children_to_include) 753 )): 754 # get back to the top of the while loop 755 # so we can finish processing the people 756 # queued up in the "not yet processed" list 757 continue 758 759 # iterate through this person's families 760 for family_handle in person.get_family_handle_list(): 761 family = self._db.get_family_from_handle(family_handle) 762 763 # queue up any children from this person's family 764 for childref in family.get_child_ref_list(): 765 child = self._db.get_person_from_handle(childref.ref) 766 children_not_yet_processed.add(child.get_handle()) 767 self._families.add(family_handle) 768 769 # include the spouse from this person's family 770 spouse_handle = utils.find_spouse(person, family) 771 if spouse_handle: 772 children_to_include.add(spouse_handle) 773 self._families.add(family_handle) 774 775 # we now merge our temp set "children_to_include" into our master set 776 self._people.update(children_to_include) 777 778 def write_people(self): 779 """ write the people """ 780 781 self.doc.add_comment('') 782 783 # If we're going to attempt to include images, then use the HTML style 784 # of .gv file. 785 use_html_output = False 786 if self._incimages: 787 use_html_output = True 788 789 # loop through all the people we need to output 790 for handle in sorted(self._people): # enable a diff 791 person = self._db.get_person_from_handle(handle) 792 name = self._name_display.display(person) 793 p_id = person.get_gramps_id() 794 795 # figure out what colour to use 796 gender = person.get_gender() 797 colour = self._colorunknown 798 if gender == Person.MALE: 799 colour = self._colormales 800 elif gender == Person.FEMALE: 801 colour = self._colorfemales 802 803 # see if we have surname colours that match this person 804 surname = person.get_primary_name().get_surname() 805 surname = surname.encode('iso-8859-1', 'xmlcharrefreplace') 806 if surname in self._surnamecolors: 807 colour = self._surnamecolors[surname] 808 809 # see if we have a birth/death or fallback dates we can use 810 if self._incdates or self._incplaces: 811 bth_event = get_birth_or_fallback(self._db, person) 812 dth_event = get_death_or_fallback(self._db, person) 813 else: 814 bth_event = None 815 dth_event = None 816 817 # output the birth or fallback event 818 birth_str = None 819 if bth_event and self._incdates: 820 date = bth_event.get_date_object() 821 if self._just_years and date.get_year_valid(): 822 birth_str = self.get_date( # localized year 823 Date(date.get_year())) 824 else: 825 birth_str = self.get_date(date) 826 827 # get birth place (one of: hamlet, village, town, city, parish, 828 # county, province, region, state or country) 829 birthplace = None 830 if bth_event and self._incplaces: 831 birthplace = self.get_event_place(bth_event) 832 833 # see if we have a deceased date we can use 834 death_str = None 835 if dth_event and self._incdates: 836 date = dth_event.get_date_object() 837 if self._just_years and date.get_year_valid(): 838 death_str = self.get_date( # localized year 839 Date(date.get_year())) 840 else: 841 death_str = self.get_date(date) 842 843 # get death place (one of: hamlet, village, town, city, parish, 844 # county, province, region, state or country) 845 deathplace = None 846 if dth_event and self._incplaces: 847 deathplace = self.get_event_place(dth_event) 848 849 # see if we have an image to use for this person 850 image_path = None 851 if self._incimages: 852 media_list = person.get_media_list() 853 if len(media_list) > 0: 854 media_handle = media_list[0].get_reference_handle() 855 media = self._db.get_media_from_handle(media_handle) 856 media_mime_type = media.get_mime_type() 857 if media_mime_type[0:5] == "image": 858 image_path = get_thumbnail_path( 859 media_path_full(self._db, media.get_path()), 860 rectangle=media_list[0].get_rectangle(), 861 size=self._imagesize) 862 863 # put the label together and output this person 864 label = "" 865 line_delimiter = '\\n' 866 if use_html_output: 867 line_delimiter = '<BR/>' 868 869 # if we have an image, then start an HTML table; 870 # remember to close the table afterwards! 871 if image_path: 872 label = ('<TABLE BORDER="0" CELLSPACING="2" CELLPADDING="0" ' 873 'CELLBORDER="0"><TR><TD><IMG SRC="%s"/></TD>' % 874 image_path) 875 if self._imageonside == 0: 876 label += '</TR><TR>' 877 label += '<TD>' 878 879 # at the very least, the label must have the person's name 880 label += html.escape(name) 881 if self.includeid == 1: # same line 882 label += " (%s)" % p_id 883 elif self.includeid == 2: # own line 884 label += "%s(%s)" % (line_delimiter, p_id) 885 886 if birth_str or death_str: 887 label += '%s(' % line_delimiter 888 if birth_str: 889 label += '%s' % birth_str 890 label += ' – ' 891 if death_str: 892 label += '%s' % death_str 893 label += ')' 894 if birthplace or deathplace: 895 if birthplace == deathplace: 896 deathplace = None # no need to print the same name twice 897 label += '%s' % line_delimiter 898 if birthplace: 899 label += '%s' % birthplace 900 if birthplace and deathplace: 901 label += ' / ' 902 if deathplace: 903 label += '%s' % deathplace 904 905 # see if we have a table that needs to be terminated 906 if image_path: 907 label += '</TD></TR></TABLE>' 908 else: 909 # non html label is enclosed by "" so escape other " 910 label = label.replace('"', '\\\"') 911 912 shape = "box" 913 style = "solid" 914 border = colour 915 fill = colour 916 917 # do not use colour if this is B&W outline 918 if self._colorize == 'outline': 919 border = "" 920 fill = "" 921 922 if gender == person.FEMALE and ("f" in self._useroundedcorners): 923 style = "rounded" 924 elif gender == person.MALE and ("m" in self._useroundedcorners): 925 style = "rounded" 926 elif gender == person.UNKNOWN: 927 shape = "hexagon" 928 929 # if we're filling the entire node: 930 if self._colorize == 'filled': 931 style += ",filled" 932 border = "" 933 934 # we're done -- add the node 935 self.doc.add_node(p_id, 936 label=label, 937 shape=shape, 938 color=border, 939 style=style, 940 fillcolor=fill, 941 htmloutput=use_html_output) 942 943 def write_families(self): 944 """ write the families """ 945 946 self.doc.add_comment('') 947 ngettext = self._locale.translation.ngettext # to see "nearby" comments 948 949 # loop through all the families we need to output 950 for family_handle in sorted(self._families): # enable a diff 951 family = self._db.get_family_from_handle(family_handle) 952 fgid = family.get_gramps_id() 953 954 # figure out a wedding date or placename we can use 955 wedding_date = None 956 wedding_place = None 957 if self._incdates or self._incplaces: 958 for event_ref in family.get_event_ref_list(): 959 event = self._db.get_event_from_handle(event_ref.ref) 960 if (event.get_type() == EventType.MARRIAGE and 961 (event_ref.get_role() == EventRoleType.FAMILY or 962 event_ref.get_role() == EventRoleType.PRIMARY)): 963 # get the wedding date 964 if self._incdates: 965 date = event.get_date_object() 966 if self._just_years and date.get_year_valid(): 967 wedding_date = self.get_date( # localized year 968 Date(date.get_year())) 969 else: 970 wedding_date = self.get_date(date) 971 # get the wedding location 972 if self._incplaces: 973 wedding_place = self.get_event_place(event) 974 break 975 976 # figure out the number of children (if any) 977 children_str = None 978 if self._incchildcount: 979 child_count = len(family.get_child_ref_list()) 980 if child_count >= 1: 981 # translators: leave all/any {...} untranslated 982 children_str = ngettext("{number_of} child", 983 "{number_of} children", child_count 984 ).format(number_of=child_count) 985 986 label = '' 987 fgid_already = False 988 if wedding_date: 989 if label != '': 990 label += '\\n' 991 label += '%s' % wedding_date 992 if self.includeid == 1 and not fgid_already: # same line 993 label += " (%s)" % fgid 994 fgid_already = True 995 if wedding_place: 996 if label != '': 997 label += '\\n' 998 label += '%s' % wedding_place 999 if self.includeid == 1 and not fgid_already: # same line 1000 label += " (%s)" % fgid 1001 fgid_already = True 1002 if self.includeid == 1 and not label: 1003 label = "(%s)" % fgid 1004 fgid_already = True 1005 elif self.includeid == 2 and not label: # own line 1006 label = "(%s)" % fgid 1007 fgid_already = True 1008 elif self.includeid == 2 and label and not fgid_already: 1009 label += "\\n(%s)" % fgid 1010 fgid_already = True 1011 if children_str: 1012 if label != '': 1013 label += '\\n' 1014 label += '%s' % children_str 1015 if self.includeid == 1 and not fgid_already: # same line 1016 label += " (%s)" % fgid 1017 fgid_already = True 1018 1019 shape = "ellipse" 1020 style = "solid" 1021 border = self._colorfamilies 1022 fill = self._colorfamilies 1023 1024 # do not use colour if this is B&W outline 1025 if self._colorize == 'outline': 1026 border = "" 1027 fill = "" 1028 1029 # if we're filling the entire node: 1030 if self._colorize == 'filled': 1031 style += ",filled" 1032 border = "" 1033 1034 # we're done -- add the node 1035 self.doc.add_node(fgid, label, shape, border, style, fill) 1036 1037 # now that we have the families written, 1038 # go ahead and link the parents and children to the families 1039 for family_handle in self._families: 1040 1041 # get the parents for this family 1042 family = self._db.get_family_from_handle(family_handle) 1043 fgid = family.get_gramps_id() 1044 father_handle = family.get_father_handle() 1045 mother_handle = family.get_mother_handle() 1046 1047 self.doc.add_comment('') 1048 1049 if self._usesubgraphs and father_handle and mother_handle: 1050 self.doc.start_subgraph(fgid) 1051 1052 # see if we have a father to link to this family 1053 if father_handle: 1054 if father_handle in self._people: 1055 father = self._db.get_person_from_handle(father_handle) 1056 father_rn = father.get_primary_name().get_regular_name() 1057 comment = self._("father: %s") % father_rn 1058 self.doc.add_link(father.get_gramps_id(), fgid, "", 1059 self._arrowheadstyle, self._arrowtailstyle, 1060 comment=comment) 1061 1062 # see if we have a mother to link to this family 1063 if mother_handle: 1064 if mother_handle in self._people: 1065 mother = self._db.get_person_from_handle(mother_handle) 1066 mother_rn = mother.get_primary_name().get_regular_name() 1067 comment = self._("mother: %s") % mother_rn 1068 self.doc.add_link(mother.get_gramps_id(), fgid, "", 1069 self._arrowheadstyle, self._arrowtailstyle, 1070 comment=comment) 1071 1072 if self._usesubgraphs and father_handle and mother_handle: 1073 self.doc.end_subgraph() 1074 1075 # link the children to the family 1076 for childref in family.get_child_ref_list(): 1077 if childref.ref in self._people: 1078 child = self._db.get_person_from_handle(childref.ref) 1079 child_rn = child.get_primary_name().get_regular_name() 1080 comment = self._("child: %s") % child_rn 1081 self.doc.add_link(fgid, child.get_gramps_id(), "", 1082 self._arrowheadstyle, self._arrowtailstyle, 1083 comment=comment) 1084 1085 def get_event_place(self, event): 1086 """ get the place of the event """ 1087 place_text = '' 1088 place_handle = event.get_place_handle() 1089 if place_handle: 1090 place = self._db.get_place_from_handle(place_handle) 1091 if place: 1092 place_text = _pd.display(self._db, place) 1093 place_text = html.escape(place_text) 1094 return place_text 1095 1096 def get_date(self, date): 1097 """ return a formatted date """ 1098 return html.escape(self._get_date(date)) 1099