1<?php 2namespace Fisharebest\Webtrees; 3 4/** 5 * webtrees: online genealogy 6 * Copyright (C) 2015 webtrees development team 7 * This program is free software: you can redistribute it and/or modify 8 * it under the terms of the GNU General Public License as published by 9 * the Free Software Foundation, either version 3 of the License, or 10 * (at your option) any later version. 11 * This program is distributed in the hope that it will be useful, 12 * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 * GNU General Public License for more details. 15 * You should have received a copy of the GNU General Public License 16 * along with this program. If not, see <http://www.gnu.org/licenses/>. 17 */ 18 19use Fisharebest\ExtCalendar\GregorianCalendar; 20use Fisharebest\Webtrees\GedcomCode\GedcomCodePedi; 21 22/** 23 * Class Individual - Class file for an individual 24 */ 25class Individual extends GedcomRecord { 26 const RECORD_TYPE = 'INDI'; 27 const URL_PREFIX = 'individual.php?pid='; 28 29 public $generation; // used in some lists to keep track of this individual’s generation in that list 30 31 /** @var Date The estimated date of birth */ 32 private $_getEstimatedBirthDate; 33 34 /** @var Date The estimated date of death */ 35 private $_getEstimatedDeathDate; 36 37 /** 38 * Sometimes, we'll know in advance that we need to load a set of records. 39 * Typically when we load families and their members. 40 * 41 * @param Tree $tree 42 * @param string[] $xrefs 43 */ 44 public static function load(Tree $tree, array $xrefs) { 45 $sql = ''; 46 $args = array( 47 'tree_id' => $tree->getTreeId(), 48 ); 49 50 foreach (array_unique($xrefs) as $n => $xref) { 51 if (!isset(self::$gedcom_record_cache[$tree->getTreeId()][$xref])) { 52 $sql .= ($n ? ',:x' : ':x') . $n; 53 $args['x' . $n] = $xref; 54 } 55 } 56 57 if (count($args) > 1) { 58 $rows = Database::prepare( 59 "SELECT i_id AS xref, i_gedcom AS gedcom" . 60 " FROM `##individuals`" . 61 " WHERE i_file = :tree_id AND i_id IN (" . $sql . ")" 62 )->execute( 63 $args 64 )->fetchAll(); 65 66 foreach ($rows as $row) { 67 self::getInstance($row->xref, $tree, $row->gedcom); 68 } 69 } 70 } 71 72 /** 73 * Can the name of this record be shown? 74 * 75 * {@inheritdoc} 76 */ 77 public function canShowName($access_level = null) { 78 if ($access_level === null) { 79 $access_level = Auth::accessLevel($this->tree); 80 } 81 82 return $this->tree->getPreference('SHOW_LIVING_NAMES') >= $access_level || $this->canShow($access_level); 83 } 84 85 /** 86 * Implement individual-specific privacy logic 87 * 88 * {@inheritdoc} 89 */ 90 protected function canShowByType($access_level) { 91 global $WT_TREE; 92 93 // Dead people... 94 if ($this->tree->getPreference('SHOW_DEAD_PEOPLE') >= $access_level && $this->isDead()) { 95 $keep_alive = false; 96 $KEEP_ALIVE_YEARS_BIRTH = $this->tree->getPreference('KEEP_ALIVE_YEARS_BIRTH'); 97 if ($KEEP_ALIVE_YEARS_BIRTH) { 98 preg_match_all('/\n1 (?:' . WT_EVENTS_BIRT . ').*(?:\n[2-9].*)*(?:\n2 DATE (.+))/', $this->gedcom, $matches, PREG_SET_ORDER); 99 foreach ($matches as $match) { 100 $date = new Date($match[1]); 101 if ($date->isOK() && $date->gregorianYear() + $KEEP_ALIVE_YEARS_BIRTH > date('Y')) { 102 $keep_alive = true; 103 break; 104 } 105 } 106 } 107 $KEEP_ALIVE_YEARS_DEATH = $this->tree->getPreference('KEEP_ALIVE_YEARS_DEATH'); 108 if ($KEEP_ALIVE_YEARS_DEATH) { 109 preg_match_all('/\n1 (?:' . WT_EVENTS_DEAT . ').*(?:\n[2-9].*)*(?:\n2 DATE (.+))/', $this->gedcom, $matches, PREG_SET_ORDER); 110 foreach ($matches as $match) { 111 $date = new Date($match[1]); 112 if ($date->isOK() && $date->gregorianYear() + $KEEP_ALIVE_YEARS_DEATH > date('Y')) { 113 $keep_alive = true; 114 break; 115 } 116 } 117 } 118 if (!$keep_alive) { 119 return true; 120 } 121 } 122 // Consider relationship privacy (unless an admin is applying download restrictions) 123 $user_path_length = $this->tree->getUserPreference(Auth::user(), 'RELATIONSHIP_PATH_LENGTH'); 124 $gedcomid = $this->tree->getUserPreference(Auth::user(), 'gedcomid'); 125 if ($gedcomid && $user_path_length && $this->tree->getTreeId() == $WT_TREE->getTreeId() && $access_level = Auth::accessLevel($this->tree)) { 126 return self::isRelated($this, $user_path_length); 127 } 128 129 // No restriction found - show living people to members only: 130 return Auth::PRIV_USER >= $access_level; 131 } 132 133 /** 134 * For relationship privacy calculations - is this individual a close relative? 135 * 136 * @param Individual $target 137 * @param int $distance 138 * 139 * @return bool 140 */ 141 private static function isRelated(Individual $target, $distance) { 142 static $cache = null; 143 144 $user_individual = self::getInstance($target->tree->getUserPreference(Auth::user(), 'gedcomid'), $target->tree); 145 if ($user_individual) { 146 if (!$cache) { 147 $cache = array( 148 0 => array($user_individual), 149 1 => array(), 150 ); 151 foreach ($user_individual->getFacts('FAM[CS]', false, Auth::PRIV_HIDE) as $fact) { 152 $family = $fact->getTarget(); 153 if ($family) { 154 $cache[1][] = $family; 155 } 156 } 157 } 158 } else { 159 // No individual linked to this account? Cannot use relationship privacy. 160 return true; 161 } 162 163 // Double the distance, as we count the INDI-FAM and FAM-INDI links separately 164 $distance *= 2; 165 166 // Consider each path length in turn 167 for ($n = 0; $n <= $distance; ++$n) { 168 if (array_key_exists($n, $cache)) { 169 // We have already calculated all records with this length 170 if ($n % 2 == 0 && in_array($target, $cache[$n], true)) { 171 return true; 172 } 173 } else { 174 // Need to calculate these paths 175 $cache[$n] = array(); 176 if ($n % 2 == 0) { 177 // Add FAM->INDI links 178 foreach ($cache[$n - 1] as $family) { 179 foreach ($family->getFacts('HUSB|WIFE|CHIL', false, Auth::PRIV_HIDE) as $fact) { 180 $individual = $fact->getTarget(); 181 // Don’t backtrack 182 if ($individual && !in_array($individual, $cache[$n - 2], true)) { 183 $cache[$n][] = $individual; 184 } 185 } 186 } 187 if (in_array($target, $cache[$n], true)) { 188 return true; 189 } 190 } else { 191 // Add INDI->FAM links 192 foreach ($cache[$n - 1] as $individual) { 193 foreach ($individual->getFacts('FAM[CS]', false, Auth::PRIV_HIDE) as $fact) { 194 $family = $fact->getTarget(); 195 // Don’t backtrack 196 if ($family && !in_array($family, $cache[$n - 2], true)) { 197 $cache[$n][] = $family; 198 } 199 } 200 } 201 } 202 } 203 } 204 205 return false; 206 } 207 208 /** {@inheritdoc} */ 209 protected function createPrivateGedcomRecord($access_level) { 210 $SHOW_PRIVATE_RELATIONSHIPS = $this->tree->getPreference('SHOW_PRIVATE_RELATIONSHIPS'); 211 212 $rec = '0 @' . $this->xref . '@ INDI'; 213 if ($this->tree->getPreference('SHOW_LIVING_NAMES') >= $access_level) { 214 // Show all the NAME tags, including subtags 215 foreach ($this->getFacts('NAME') as $fact) { 216 $rec .= "\n" . $fact->getGedcom(); 217 } 218 } 219 // Just show the 1 FAMC/FAMS tag, not any subtags, which may contain private data 220 preg_match_all('/\n1 (?:FAMC|FAMS) @(' . WT_REGEX_XREF . ')@/', $this->gedcom, $matches, PREG_SET_ORDER); 221 foreach ($matches as $match) { 222 $rela = Family::getInstance($match[1], $this->tree); 223 if ($rela && ($SHOW_PRIVATE_RELATIONSHIPS || $rela->canShow($access_level))) { 224 $rec .= $match[0]; 225 } 226 } 227 // Don’t privatize sex. 228 if (preg_match('/\n1 SEX [MFU]/', $this->gedcom, $match)) { 229 $rec .= $match[0]; 230 } 231 232 return $rec; 233 } 234 235 /** {@inheritdoc} */ 236 protected static function fetchGedcomRecord($xref, $tree_id) { 237 return Database::prepare( 238 "SELECT i_gedcom FROM `##individuals` WHERE i_id = :xref AND i_file = :tree_id" 239 )->execute(array( 240 'xref' => $xref, 241 'tree_id' => $tree_id, 242 ))->fetchOne(); 243 } 244 245 /** 246 * Static helper function to sort an array of people by birth date 247 * 248 * @param Individual $x 249 * @param Individual $y 250 * 251 * @return int 252 */ 253 public static function compareBirthDate(Individual $x, Individual $y) { 254 return Date::compare($x->getEstimatedBirthDate(), $y->getEstimatedBirthDate()); 255 } 256 257 /** 258 * Static helper function to sort an array of people by death date 259 * 260 * @param Individual $x 261 * @param Individual $y 262 * 263 * @return int 264 */ 265 public static function compareDeathDate(Individual $x, Individual $y) { 266 return Date::compare($x->getEstimatedDeathDate(), $y->getEstimatedDeathDate()); 267 } 268 269 /** 270 * Calculate whether this individual is living or dead. 271 * If not known to be dead, then assume living. 272 * 273 * @return bool 274 */ 275 public function isDead() { 276 $MAX_ALIVE_AGE = $this->tree->getPreference('MAX_ALIVE_AGE'); 277 278 // "1 DEAT Y" or "1 DEAT/2 DATE" or "1 DEAT/2 PLAC" 279 if (preg_match('/\n1 (?:' . WT_EVENTS_DEAT . ')(?: Y|(?:\n[2-9].+)*\n2 (DATE|PLAC) )/', $this->gedcom)) { 280 return true; 281 } 282 283 // If any event occured more than $MAX_ALIVE_AGE years ago, then assume the individual is dead 284 if (preg_match_all('/\n2 DATE (.+)/', $this->gedcom, $date_matches)) { 285 foreach ($date_matches[1] as $date_match) { 286 $date = new Date($date_match); 287 if ($date->isOK() && $date->maximumJulianDay() <= WT_CLIENT_JD - 365 * $MAX_ALIVE_AGE) { 288 return true; 289 } 290 } 291 // The individual has one or more dated events. All are less than $MAX_ALIVE_AGE years ago. 292 // If one of these is a birth, the individual must be alive. 293 if (preg_match('/\n1 BIRT(?:\n[2-9].+)*\n2 DATE /', $this->gedcom)) { 294 return false; 295 } 296 } 297 298 // If we found no conclusive dates then check the dates of close relatives. 299 300 // Check parents (birth and adopted) 301 foreach ($this->getChildFamilies(Auth::PRIV_HIDE) as $family) { 302 foreach ($family->getSpouses(Auth::PRIV_HIDE) as $parent) { 303 // Assume parents are no more than 45 years older than their children 304 preg_match_all('/\n2 DATE (.+)/', $parent->gedcom, $date_matches); 305 foreach ($date_matches[1] as $date_match) { 306 $date = new Date($date_match); 307 if ($date->isOK() && $date->maximumJulianDay() <= WT_CLIENT_JD - 365 * ($MAX_ALIVE_AGE + 45)) { 308 return true; 309 } 310 } 311 } 312 } 313 314 // Check spouses 315 foreach ($this->getSpouseFamilies(Auth::PRIV_HIDE) as $family) { 316 preg_match_all('/\n2 DATE (.+)/', $family->gedcom, $date_matches); 317 foreach ($date_matches[1] as $date_match) { 318 $date = new Date($date_match); 319 // Assume marriage occurs after age of 10 320 if ($date->isOK() && $date->maximumJulianDay() <= WT_CLIENT_JD - 365 * ($MAX_ALIVE_AGE - 10)) { 321 return true; 322 } 323 } 324 // Check spouse dates 325 $spouse = $family->getSpouse($this); 326 if ($spouse) { 327 preg_match_all('/\n2 DATE (.+)/', $spouse->gedcom, $date_matches); 328 foreach ($date_matches[1] as $date_match) { 329 $date = new Date($date_match); 330 // Assume max age difference between spouses of 40 years 331 if ($date->isOK() && $date->maximumJulianDay() <= WT_CLIENT_JD - 365 * ($MAX_ALIVE_AGE + 40)) { 332 return true; 333 } 334 } 335 } 336 // Check child dates 337 foreach ($family->getChildren(Auth::PRIV_HIDE) as $child) { 338 preg_match_all('/\n2 DATE (.+)/', $child->gedcom, $date_matches); 339 // Assume children born after age of 15 340 foreach ($date_matches[1] as $date_match) { 341 $date = new Date($date_match); 342 if ($date->isOK() && $date->maximumJulianDay() <= WT_CLIENT_JD - 365 * ($MAX_ALIVE_AGE - 15)) { 343 return true; 344 } 345 } 346 // Check grandchildren 347 foreach ($child->getSpouseFamilies(Auth::PRIV_HIDE) as $child_family) { 348 foreach ($child_family->getChildren(Auth::PRIV_HIDE) as $grandchild) { 349 preg_match_all('/\n2 DATE (.+)/', $grandchild->gedcom, $date_matches); 350 // Assume grandchildren born after age of 30 351 foreach ($date_matches[1] as $date_match) { 352 $date = new Date($date_match); 353 if ($date->isOK() && $date->maximumJulianDay() <= WT_CLIENT_JD - 365 * ($MAX_ALIVE_AGE - 30)) { 354 return true; 355 } 356 } 357 } 358 } 359 } 360 } 361 362 return false; 363 } 364 365 /** 366 * Find the highlighted media object for an individual 367 * 1. Ignore all media objects that are not displayable because of Privacy rules 368 * 2. Ignore all media objects with the Highlight option set to "N" 369 * 3. Pick the first media object that matches these criteria, in order of preference: 370 * (a) Level 1 object with the Highlight option set to "Y" 371 * (b) Level 1 object with the Highlight option missing or set to other than "Y" or "N" 372 * (c) Level 2 or higher object with the Highlight option set to "Y" 373 * 374 * @return null|Media 375 */ 376 public function findHighlightedMedia() { 377 $objectA = null; 378 $objectB = null; 379 $objectC = null; 380 381 // Iterate over all of the media items for the individual 382 preg_match_all('/\n(\d) OBJE @(' . WT_REGEX_XREF . ')@/', $this->getGedcom(), $matches, PREG_SET_ORDER); 383 foreach ($matches as $match) { 384 $media = Media::getInstance($match[2], $this->tree); 385 if (!$media || !$media->canShow() || $media->isExternal()) { 386 continue; 387 } 388 $level = $match[1]; 389 $prim = $media->isPrimary(); 390 if ($prim == 'N') { 391 continue; 392 } 393 if ($level == 1) { 394 if ($prim == 'Y') { 395 if (empty($objectA)) { 396 $objectA = $media; 397 } 398 } else { 399 if (empty($objectB)) { 400 $objectB = $media; 401 } 402 } 403 } else { 404 if ($prim == 'Y') { 405 if (empty($objectC)) { 406 $objectC = $media; 407 } 408 } 409 } 410 } 411 412 if ($objectA) { 413 return $objectA; 414 } 415 if ($objectB) { 416 return $objectB; 417 } 418 if ($objectC) { 419 return $objectC; 420 } 421 422 return null; 423 } 424 425 /** 426 * Display the prefered image for this individual. 427 * Use an icon if no image is available. 428 * 429 * @return string 430 */ 431 public function displayImage() { 432 $media = $this->findHighlightedMedia(); 433 if ($media) { 434 // Thumbnail exists - use it. 435 return $media->displayImage(); 436 } elseif ($this->tree->getPreference('USE_SILHOUETTE')) { 437 // No thumbnail exists - use an icon 438 return '<i class="icon-silhouette-' . $this->getSex() . '"></i>'; 439 } else { 440 return ''; 441 } 442 } 443 444 /** 445 * Get the date of birth 446 * 447 * @return Date 448 */ 449 public function getBirthDate() { 450 foreach ($this->getAllBirthDates() as $date) { 451 if ($date->isOK()) { 452 return $date; 453 } 454 } 455 456 return new Date(''); 457 } 458 459 /** 460 * Get the place of birth 461 * 462 * @return string 463 */ 464 public function getBirthPlace() { 465 foreach ($this->getAllBirthPlaces() as $place) { 466 if ($place) { 467 return $place; 468 } 469 } 470 471 return ''; 472 } 473 474 /** 475 * Get the year of birth 476 * 477 * @return string the year of birth 478 */ 479 public function getBirthYear() { 480 return $this->getBirthDate()->minimumDate()->format('%Y'); 481 } 482 483 /** 484 * Get the date of death 485 * 486 * @return Date 487 */ 488 public function getDeathDate() { 489 foreach ($this->getAllDeathDates() as $date) { 490 if ($date->isOK()) { 491 return $date; 492 } 493 } 494 495 return new Date(''); 496 } 497 498 /** 499 * Get the place of death 500 * 501 * @return string 502 */ 503 public function getDeathPlace() { 504 foreach ($this->getAllDeathPlaces() as $place) { 505 if ($place) { 506 return $place; 507 } 508 } 509 510 return ''; 511 } 512 513 /** 514 * get the death year 515 * 516 * @return string the year of death 517 */ 518 public function getDeathYear() { 519 return $this->getDeathDate()->minimumDate()->format('%Y'); 520 } 521 522 /** 523 * Get the range of years in which a individual lived. e.g. “1870–”, “1870–1920”, “–1920”. 524 * Provide the full date using a tooltip. 525 * For consistent layout in charts, etc., show just a “–” when no dates are known. 526 * Note that this is a (non-breaking) en-dash, and not a hyphen. 527 * 528 * @return string 529 */ 530 public function getLifeSpan() { 531 return 532 /* I18N: A range of years, e.g. “1870–”, “1870–1920”, “–1920” */ I18N::translate( 533 '%1$s–%2$s', 534 '<span title="' . strip_tags($this->getBirthDate()->display()) . '">' . $this->getBirthDate()->minimumDate()->format('%Y') . '</span>', 535 '<span title="' . strip_tags($this->getDeathDate()->display()) . '">' . $this->getDeathDate()->minimumDate()->format('%Y') . '</span>' 536 ); 537 } 538 539 /** 540 * Get all the birth dates - for the individual lists. 541 * 542 * @return Date[] 543 */ 544 public function getAllBirthDates() { 545 foreach (explode('|', WT_EVENTS_BIRT) as $event) { 546 $tmp = $this->getAllEventDates($event); 547 if ($tmp) { 548 return $tmp; 549 } 550 } 551 552 return array(); 553 } 554 555 /** 556 * Gat all the birth places - for the individual lists. 557 * 558 * @return string[] 559 */ 560 public function getAllBirthPlaces() { 561 foreach (explode('|', WT_EVENTS_BIRT) as $event) { 562 $tmp = $this->getAllEventPlaces($event); 563 if ($tmp) { 564 return $tmp; 565 } 566 } 567 568 return array(); 569 } 570 571 /** 572 * Get all the death dates - for the individual lists. 573 * 574 * @return Date[] 575 */ 576 public function getAllDeathDates() { 577 foreach (explode('|', WT_EVENTS_DEAT) as $event) { 578 $tmp = $this->getAllEventDates($event); 579 if ($tmp) { 580 return $tmp; 581 } 582 } 583 584 return array(); 585 } 586 587 /** 588 * Get all the death places - for the individual lists. 589 * 590 * @return string[] 591 */ 592 public function getAllDeathPlaces() { 593 foreach (explode('|', WT_EVENTS_DEAT) as $event) { 594 $tmp = $this->getAllEventPlaces($event); 595 if ($tmp) { 596 return $tmp; 597 } 598 } 599 600 return array(); 601 } 602 603 /** 604 * Generate an estimate for the date of birth, based on dates of parents/children/spouses 605 * 606 * @return Date 607 */ 608 public function getEstimatedBirthDate() { 609 if (is_null($this->_getEstimatedBirthDate)) { 610 foreach ($this->getAllBirthDates() as $date) { 611 if ($date->isOK()) { 612 $this->_getEstimatedBirthDate = $date; 613 break; 614 } 615 } 616 if (is_null($this->_getEstimatedBirthDate)) { 617 $min = array(); 618 $max = array(); 619 $tmp = $this->getDeathDate(); 620 if ($tmp->isOK()) { 621 $min[] = $tmp->minimumJulianDay() - $this->tree->getPreference('MAX_ALIVE_AGE') * 365; 622 $max[] = $tmp->maximumJulianDay(); 623 } 624 foreach ($this->getChildFamilies() as $family) { 625 $tmp = $family->getMarriageDate(); 626 if ($tmp->isOK()) { 627 $min[] = $tmp->maximumJulianDay() - 365 * 1; 628 $max[] = $tmp->minimumJulianDay() + 365 * 30; 629 } 630 if ($parent = $family->getHusband()) { 631 $tmp = $parent->getBirthDate(); 632 if ($tmp->isOK()) { 633 $min[] = $tmp->maximumJulianDay() + 365 * 15; 634 $max[] = $tmp->minimumJulianDay() + 365 * 65; 635 } 636 } 637 if ($parent = $family->getWife()) { 638 $tmp = $parent->getBirthDate(); 639 if ($tmp->isOK()) { 640 $min[] = $tmp->maximumJulianDay() + 365 * 15; 641 $max[] = $tmp->minimumJulianDay() + 365 * 45; 642 } 643 } 644 foreach ($family->getChildren() as $child) { 645 $tmp = $child->getBirthDate(); 646 if ($tmp->isOK()) { 647 $min[] = $tmp->maximumJulianDay() - 365 * 30; 648 $max[] = $tmp->minimumJulianDay() + 365 * 30; 649 } 650 } 651 } 652 foreach ($this->getSpouseFamilies() as $family) { 653 $tmp = $family->getMarriageDate(); 654 if ($tmp->isOK()) { 655 $min[] = $tmp->maximumJulianDay() - 365 * 45; 656 $max[] = $tmp->minimumJulianDay() - 365 * 15; 657 } 658 $spouse = $family->getSpouse($this); 659 if ($spouse) { 660 $tmp = $spouse->getBirthDate(); 661 if ($tmp->isOK()) { 662 $min[] = $tmp->maximumJulianDay() - 365 * 25; 663 $max[] = $tmp->minimumJulianDay() + 365 * 25; 664 } 665 } 666 foreach ($family->getChildren() as $child) { 667 $tmp = $child->getBirthDate(); 668 if ($tmp->isOK()) { 669 $min[] = $tmp->maximumJulianDay() - 365 * ($this->getSex() == 'F' ? 45 : 65); 670 $max[] = $tmp->minimumJulianDay() - 365 * 15; 671 } 672 } 673 } 674 if ($min && $max) { 675 $gregorian_calendar = new GregorianCalendar; 676 677 list($year) = $gregorian_calendar->jdToYmd((int) ((max($min) + min($max)) / 2)); 678 $this->_getEstimatedBirthDate = new Date('EST ' . $year); 679 } else { 680 $this->_getEstimatedBirthDate = new Date(''); // always return a date object 681 } 682 } 683 } 684 685 return $this->_getEstimatedBirthDate; 686 } 687 688 /** 689 * Generate an estimated date of death. 690 * 691 * @return Date 692 */ 693 public function getEstimatedDeathDate() { 694 if ($this->_getEstimatedDeathDate === null) { 695 foreach ($this->getAllDeathDates() as $date) { 696 if ($date->isOK()) { 697 $this->_getEstimatedDeathDate = $date; 698 break; 699 } 700 } 701 if ($this->_getEstimatedDeathDate === null) { 702 if ($this->getEstimatedBirthDate()->minimumJulianDay()) { 703 $this->_getEstimatedDeathDate = $this->getEstimatedBirthDate()->addYears($this->tree->getPreference('MAX_ALIVE_AGE'), 'BEF'); 704 } else { 705 $this->_getEstimatedDeathDate = new Date(''); // always return a date object 706 } 707 } 708 } 709 710 return $this->_getEstimatedDeathDate; 711 } 712 713 /** 714 * Get the sex - M F or U 715 * Use the un-privatised gedcom record. We call this function during 716 * the privatize-gedcom function, and we are allowed to know this. 717 * 718 * @return string 719 */ 720 public function getSex() { 721 if (preg_match('/\n1 SEX ([MF])/', $this->gedcom . $this->pending, $match)) { 722 return $match[1]; 723 } else { 724 return 'U'; 725 } 726 } 727 728 /** 729 * Get the individual’s sex image 730 * 731 * @param string $size 732 * 733 * @return string 734 */ 735 public function getSexImage($size = 'small') { 736 return self::sexImage($this->getSex(), $size); 737 } 738 739 /** 740 * Generate a sex icon/image 741 * 742 * @param string $sex 743 * @param string $size 744 * 745 * @return string 746 */ 747 public static function sexImage($sex, $size = 'small') { 748 return '<i class="icon-sex_' . strtolower($sex) . '_' . ($size == 'small' ? '9x9' : '15x15') . '"></i>'; 749 } 750 751 /** 752 * Generate the CSS class to be used for drawing this individual 753 * 754 * @return string 755 */ 756 public function getBoxStyle() { 757 $tmp = array('M' => '', 'F' => 'F', 'U' => 'NN'); 758 759 return 'person_box' . $tmp[$this->getSex()]; 760 } 761 762 /** 763 * Get a list of this individual’s spouse families 764 * 765 * @param int|null $access_level 766 * 767 * @return Family[] 768 */ 769 public function getSpouseFamilies($access_level = null) { 770 if ($access_level === null) { 771 $access_level = Auth::accessLevel($this->tree); 772 } 773 774 $SHOW_PRIVATE_RELATIONSHIPS = $this->tree->getPreference('SHOW_PRIVATE_RELATIONSHIPS'); 775 776 $families = array(); 777 foreach ($this->getFacts('FAMS', false, $access_level, $SHOW_PRIVATE_RELATIONSHIPS) as $fact) { 778 $family = $fact->getTarget(); 779 if ($family && ($SHOW_PRIVATE_RELATIONSHIPS || $family->canShow($access_level))) { 780 $families[] = $family; 781 } 782 } 783 784 return $families; 785 } 786 787 /** 788 * Get the current spouse of this individual. 789 * 790 * Where an individual has multiple spouses, assume they are stored 791 * in chronological order, and take the last one found. 792 * 793 * @return Individual|null 794 */ 795 public function getCurrentSpouse() { 796 $tmp = $this->getSpouseFamilies(); 797 $family = end($tmp); 798 if ($family) { 799 return $family->getSpouse($this); 800 } else { 801 return null; 802 } 803 } 804 805 /** 806 * Count the children belonging to this individual. 807 * 808 * @return int 809 */ 810 public function getNumberOfChildren() { 811 if (preg_match('/\n1 NCHI (\d+)(?:\n|$)/', $this->getGedcom(), $match)) { 812 return $match[1]; 813 } else { 814 $children = array(); 815 foreach ($this->getSpouseFamilies() as $fam) { 816 foreach ($fam->getChildren() as $child) { 817 $children[$child->getXref()] = true; 818 } 819 } 820 821 return count($children); 822 } 823 } 824 825 /** 826 * Get a list of this individual’s child families (i.e. their parents). 827 * 828 * @param int|null $access_level 829 * 830 * @return Family[] 831 */ 832 public function getChildFamilies($access_level = null) { 833 if ($access_level === null) { 834 $access_level = Auth::accessLevel($this->tree); 835 } 836 837 $SHOW_PRIVATE_RELATIONSHIPS = $this->tree->getPreference('SHOW_PRIVATE_RELATIONSHIPS'); 838 839 $families = array(); 840 foreach ($this->getFacts('FAMC', false, $access_level, $SHOW_PRIVATE_RELATIONSHIPS) as $fact) { 841 $family = $fact->getTarget(); 842 if ($family && ($SHOW_PRIVATE_RELATIONSHIPS || $family->canShow($access_level))) { 843 $families[] = $family; 844 } 845 } 846 847 return $families; 848 } 849 850 /** 851 * Get the preferred parents for this individual. 852 * 853 * An individual may multiple parents (e.g. birth, adopted, disputed). 854 * The preferred family record is: 855 * (a) the first one with an explicit tag "_PRIMARY Y" 856 * (b) the first one with a pedigree of "birth" 857 * (c) the first one with no pedigree (default is "birth") 858 * (d) the first one found 859 * 860 * @return Family|null 861 */ 862 public function getPrimaryChildFamily() { 863 $families = $this->getChildFamilies(); 864 switch (count($families)) { 865 case 0: 866 return null; 867 case 1: 868 return reset($families); 869 default: 870 // If there is more than one FAMC record, choose the preferred parents: 871 // a) records with '2 _PRIMARY' 872 foreach ($families as $famid => $fam) { 873 if (preg_match("/\n1 FAMC @{$famid}@\n(?:[2-9].*\n)*(?:2 _PRIMARY Y)/", $this->getGedcom())) { 874 return $fam; 875 } 876 } 877 // b) records with '2 PEDI birt' 878 foreach ($families as $famid => $fam) { 879 if (preg_match("/\n1 FAMC @{$famid}@\n(?:[2-9].*\n)*(?:2 PEDI birth)/", $this->getGedcom())) { 880 return $fam; 881 } 882 } 883 // c) records with no '2 PEDI' 884 foreach ($families as $famid => $fam) { 885 if (!preg_match("/\n1 FAMC @{$famid}@\n(?:[2-9].*\n)*(?:2 PEDI)/", $this->getGedcom())) { 886 return $fam; 887 } 888 } 889 890 // d) any record 891 return reset($families); 892 } 893 } 894 895 /** 896 * Get a list of step-parent families. 897 * 898 * @return Family[] 899 */ 900 public function getChildStepFamilies() { 901 $step_families = array(); 902 $families = $this->getChildFamilies(); 903 foreach ($families as $family) { 904 $father = $family->getHusband(); 905 if ($father) { 906 foreach ($father->getSpouseFamilies() as $step_family) { 907 if (!in_array($step_family, $families, true)) { 908 $step_families[] = $step_family; 909 } 910 } 911 } 912 $mother = $family->getWife(); 913 if ($mother) { 914 foreach ($mother->getSpouseFamilies() as $step_family) { 915 if (!in_array($step_family, $families, true)) { 916 $step_families[] = $step_family; 917 } 918 } 919 } 920 } 921 922 return $step_families; 923 } 924 925 /** 926 * Get a list of step-parent families. 927 * 928 * @return Family[] 929 */ 930 public function getSpouseStepFamilies() { 931 $step_families = array(); 932 $families = $this->getSpouseFamilies(); 933 foreach ($families as $family) { 934 $spouse = $family->getSpouse($this); 935 if ($spouse) { 936 foreach ($family->getSpouse($this)->getSpouseFamilies() as $step_family) { 937 if (!in_array($step_family, $families, true)) { 938 $step_families[] = $step_family; 939 } 940 } 941 } 942 } 943 944 return $step_families; 945 } 946 947 /** 948 * A label for a parental family group 949 * 950 * @param Family $family 951 * 952 * @return string 953 */ 954 public function getChildFamilyLabel(Family $family) { 955 if (preg_match('/\n1 FAMC @' . $family->getXref() . '@(?:\n[2-9].*)*\n2 PEDI (.+)/', $this->getGedcom(), $match)) { 956 // A specified pedigree 957 return GedcomCodePedi::getChildFamilyLabel($match[1]); 958 } else { 959 // Default (birth) pedigree 960 return GedcomCodePedi::getChildFamilyLabel(''); 961 } 962 } 963 964 /** 965 * Create a label for a step family 966 * 967 * @param Family $step_family 968 * 969 * @return string 970 */ 971 public function getStepFamilyLabel(Family $step_family) { 972 foreach ($this->getChildFamilies() as $family) { 973 if ($family !== $step_family) { 974 // Must be a step-family 975 foreach ($family->getSpouses() as $parent) { 976 foreach ($step_family->getSpouses() as $step_parent) { 977 if ($parent === $step_parent) { 978 // One common parent - must be a step family 979 if ($parent->getSex() == 'M') { 980 // Father’s family with someone else 981 if ($step_family->getSpouse($step_parent)) { 982 return 983 /* I18N: A step-family. %s is an individual’s name */ 984 I18N::translate('Father’s family with %s', $step_family->getSpouse($step_parent)->getFullName()); 985 } else { 986 return 987 /* I18N: A step-family. */ 988 I18N::translate('Father’s family with an unknown individual'); 989 } 990 } else { 991 // Mother’s family with someone else 992 if ($step_family->getSpouse($step_parent)) { 993 return 994 /* I18N: A step-family. %s is an individual’s name */ 995 I18N::translate('Mother’s family with %s', $step_family->getSpouse($step_parent)->getFullName()); 996 } else { 997 return 998 /* I18N: A step-family. */ 999 I18N::translate('Mother’s family with an unknown individual'); 1000 } 1001 } 1002 } 1003 } 1004 } 1005 } 1006 } 1007 1008 // Perahps same parents - but a different family record? 1009 return I18N::translate('Family with parents'); 1010 } 1011 1012 /** 1013 * @todo this function does not belong in this class 1014 * 1015 * @param Family $family 1016 * 1017 * @return string 1018 */ 1019 public function getSpouseFamilyLabel(Family $family) { 1020 $spouse = $family->getSpouse($this); 1021 if ($spouse) { 1022 return 1023 /* I18N: %s is the spouse name */ 1024 I18N::translate('Family with %s', $spouse->getFullName()); 1025 } else { 1026 return $family->getFullName(); 1027 } 1028 } 1029 1030 /** 1031 * get primary parents names for this individual 1032 * 1033 * @param string $classname optional css class 1034 * @param string $display optional css style display 1035 * 1036 * @return string a div block with father & mother names 1037 */ 1038 public function getPrimaryParentsNames($classname = '', $display = '') { 1039 $fam = $this->getPrimaryChildFamily(); 1040 if (!$fam) { 1041 return ''; 1042 } 1043 $txt = '<div'; 1044 if ($classname) { 1045 $txt .= " class=\"$classname\""; 1046 } 1047 if ($display) { 1048 $txt .= " style=\"display:$display\""; 1049 } 1050 $txt .= '>'; 1051 $husb = $fam->getHusband(); 1052 if ($husb) { 1053 // Temporarily reset the 'prefered' display name, as we always 1054 // want the default name, not the one selected for display on the indilist. 1055 $primary = $husb->getPrimaryName(); 1056 $husb->setPrimaryName(null); 1057 $txt .= 1058 /* I18N: %s is the name of an individual’s father */ 1059 I18N::translate('Father: %s', $husb->getFullName()) . '<br>'; 1060 $husb->setPrimaryName($primary); 1061 } 1062 $wife = $fam->getWife(); 1063 if ($wife) { 1064 // Temporarily reset the 'prefered' display name, as we always 1065 // want the default name, not the one selected for display on the indilist. 1066 $primary = $wife->getPrimaryName(); 1067 $wife->setPrimaryName(null); 1068 $txt .= 1069 /* I18N: %s is the name of an individual’s mother */ 1070 I18N::translate('Mother: %s', $wife->getFullName()); 1071 $wife->setPrimaryName($primary); 1072 } 1073 $txt .= '</div>'; 1074 1075 return $txt; 1076 } 1077 1078 /** {@inheritdoc} */ 1079 public function getFallBackName() { 1080 return '@P.N. /@N.N./'; 1081 } 1082 1083 /** 1084 * Convert a name record into ‘full’ and ‘sort’ versions. 1085 * Use the NAME field to generate the ‘full’ version, as the 1086 * gedcom spec says that this is the individual’s name, as they would write it. 1087 * Use the SURN field to generate the sortable names. Note that this field 1088 * may also be used for the ‘true’ surname, perhaps spelt differently to that 1089 * recorded in the NAME field. e.g. 1090 * 1091 * 1 NAME Robert /de Gliderow/ 1092 * 2 GIVN Robert 1093 * 2 SPFX de 1094 * 2 SURN CLITHEROW 1095 * 2 NICK The Bald 1096 * 1097 * full=>'Robert de Gliderow 'The Bald'' 1098 * sort=>'CLITHEROW, ROBERT' 1099 * 1100 * Handle multiple surnames, either as; 1101 * 1102 * 1 NAME Carlos /Vasquez/ y /Sante/ 1103 * or 1104 * 1 NAME Carlos /Vasquez y Sante/ 1105 * 2 GIVN Carlos 1106 * 2 SURN Vasquez,Sante 1107 * 1108 * @param string $type 1109 * @param string $full 1110 * @param string $gedcom 1111 */ 1112 protected function addName($type, $full, $gedcom) { 1113 global $UNKNOWN_NN, $UNKNOWN_PN; 1114 1115 //////////////////////////////////////////////////////////////////////////// 1116 // Extract the structured name parts - use for "sortable" names and indexes 1117 //////////////////////////////////////////////////////////////////////////// 1118 1119 $sublevel = 1 + (int) $gedcom[0]; 1120 $NPFX = preg_match("/\n{$sublevel} NPFX (.+)/", $gedcom, $match) ? $match[1] : ''; 1121 $GIVN = preg_match("/\n{$sublevel} GIVN (.+)/", $gedcom, $match) ? $match[1] : ''; 1122 $SURN = preg_match("/\n{$sublevel} SURN (.+)/", $gedcom, $match) ? $match[1] : ''; 1123 $NSFX = preg_match("/\n{$sublevel} NSFX (.+)/", $gedcom, $match) ? $match[1] : ''; 1124 $NICK = preg_match("/\n{$sublevel} NICK (.+)/", $gedcom, $match) ? $match[1] : ''; 1125 1126 // SURN is an comma-separated list of surnames... 1127 if ($SURN) { 1128 $SURNS = preg_split('/ *, */', $SURN); 1129 } else { 1130 $SURNS = array(); 1131 } 1132 // ...so is GIVN - but nobody uses it like that 1133 $GIVN = str_replace('/ *, */', ' ', $GIVN); 1134 1135 //////////////////////////////////////////////////////////////////////////// 1136 // Extract the components from NAME - use for the "full" names 1137 //////////////////////////////////////////////////////////////////////////// 1138 1139 // Fix bad slashes. e.g. 'John/Smith' => 'John/Smith/' 1140 if (substr_count($full, '/') % 2 == 1) { 1141 $full = $full . '/'; 1142 } 1143 1144 // GEDCOM uses "//" to indicate an unknown surname 1145 $full = preg_replace('/\/\//', '/@N.N./', $full); 1146 1147 // Extract the surname. 1148 // Note, there may be multiple surnames, e.g. Jean /Vasquez/ y /Cortes/ 1149 if (preg_match('/\/.*\//', $full, $match)) { 1150 $surname = str_replace('/', '', $match[0]); 1151 } else { 1152 $surname = ''; 1153 } 1154 1155 // If we don’t have a SURN record, extract it from the NAME 1156 if (!$SURNS) { 1157 if (preg_match_all('/\/([^\/]*)\//', $full, $matches)) { 1158 // There can be many surnames, each wrapped with '/' 1159 $SURNS = $matches[1]; 1160 foreach ($SURNS as $n => $SURN) { 1161 // Remove surname prefixes, such as "van de ", "d'" and "'t " (lower case only) 1162 $SURNS[$n] = preg_replace('/^(?:[a-z]+ |[a-z]+\' ?|\'[a-z]+ )+/', '', $SURN); 1163 } 1164 } else { 1165 // It is valid not to have a surname at all 1166 $SURNS = array(''); 1167 } 1168 } 1169 1170 // If we don’t have a GIVN record, extract it from the NAME 1171 if (!$GIVN) { 1172 $GIVN = preg_replace( 1173 array( 1174 '/ ?\/.*\/ ?/', // remove surname 1175 '/ ?".+"/', // remove nickname 1176 '/ {2,}/', // multiple spaces, caused by the above 1177 '/^ | $/', // leading/trailing spaces, caused by the above 1178 ), 1179 array( 1180 ' ', 1181 ' ', 1182 ' ', 1183 '', 1184 ), 1185 $full 1186 ); 1187 } 1188 1189 // Add placeholder for unknown given name 1190 if (!$GIVN) { 1191 $GIVN = '@P.N.'; 1192 $pos = strpos($full, '/'); 1193 $full = substr($full, 0, $pos) . '@P.N. ' . substr($full, $pos); 1194 } 1195 1196 // The NPFX field might be present, but not appear in the NAME 1197 if ($NPFX && strpos($full, "$NPFX ") !== 0) { 1198 $full = "$NPFX $full"; 1199 } 1200 1201 // The NSFX field might be present, but not appear in the NAME 1202 if ($NSFX && strrpos($full, " $NSFX") !== strlen($full) - strlen(" $NSFX")) { 1203 $full = "$full $NSFX"; 1204 } 1205 1206 // GEDCOM nicknames should be specificied in a NICK field, or in the 1207 // NAME filed, surrounded by ASCII quotes (or both). 1208 if ($NICK && strpos($full, '"' . $NICK . '"') === false) { 1209 // A NICK field is present, but not included in the NAME. 1210 $pos = strpos($full, '/'); 1211 if ($pos === false) { 1212 // No surname - just append it 1213 $full .= ' "' . $NICK . '"'; 1214 } else { 1215 // Insert before surname 1216 $full = substr($full, 0, $pos) . '"' . $NICK . '" ' . substr($full, $pos); 1217 } 1218 } 1219 1220 // Remove slashes - they don’t get displayed 1221 // $fullNN keeps the @N.N. placeholders, for the database 1222 // $full is for display on-screen 1223 $fullNN = str_replace('/', '', $full); 1224 1225 // Insert placeholders for any missing/unknown names 1226 $full = str_replace('@N.N.', $UNKNOWN_NN, $full); 1227 $full = str_replace('@P.N.', $UNKNOWN_PN, $full); 1228 // Localise quotation marks around the nickname 1229 $full = preg_replace_callback('/"([^"]*)"/', function ($matches) { return I18N::translate('“%s”', $matches[1]); }, $full); 1230 // Format for display 1231 $full = '<span class="NAME" dir="auto" translate="no">' . preg_replace('/\/([^\/]*)\//', '<span class="SURN">$1</span>', Filter::escapeHtml($full)) . '</span>'; 1232 1233 // A suffix of “*” indicates a preferred name 1234 $full = preg_replace('/([^ >]*)\*/', '<span class="starredname">\\1</span>', $full); 1235 1236 // Remove prefered-name indicater - they don’t go in the database 1237 $GIVN = str_replace('*', '', $GIVN); 1238 $fullNN = str_replace('*', '', $fullNN); 1239 1240 foreach ($SURNS as $SURN) { 1241 // Scottish 'Mc and Mac ' prefixes both sort under 'Mac' 1242 if (strcasecmp(substr($SURN, 0, 2), 'Mc') == 0) { 1243 $SURN = substr_replace($SURN, 'Mac', 0, 2); 1244 } elseif (strcasecmp(substr($SURN, 0, 4), 'Mac ') == 0) { 1245 $SURN = substr_replace($SURN, 'Mac', 0, 4); 1246 } 1247 1248 $this->_getAllNames[] = array( 1249 'type' => $type, 1250 'sort' => $SURN . ',' . $GIVN, 1251 'full' => $full, // This is used for display 1252 'fullNN' => $fullNN, // This goes into the database 1253 'surname' => $surname, // This goes into the database 1254 'givn' => $GIVN, // This goes into the database 1255 'surn' => $SURN, // This goes into the database 1256 ); 1257 } 1258 } 1259 1260 /** 1261 * Get an array of structures containing all the names in the record 1262 */ 1263 public function extractNames() { 1264 $this->extractNamesFromFacts(1, 'NAME', $this->getFacts('NAME', false, Auth::accessLevel($this->tree), $this->canShowName())); 1265 } 1266 1267 /** 1268 * Extra info to display when displaying this record in a list of 1269 * selection items or favorites. 1270 * 1271 * @return string 1272 */ 1273 public function formatListDetails() { 1274 return 1275 $this->formatFirstMajorFact(WT_EVENTS_BIRT, 1) . 1276 $this->formatFirstMajorFact(WT_EVENTS_DEAT, 1); 1277 } 1278 1279 /** 1280 * Create a short name for compact display on charts 1281 * 1282 * @return string 1283 */ 1284 public function getShortName() { 1285 global $bwidth, $UNKNOWN_NN, $UNKNOWN_PN; 1286 1287 // Estimate number of characters that can fit in box. Calulates to 28 characters in webtrees theme, or 34 if no thumbnail used. 1288 if ($this->tree->getPreference('SHOW_HIGHLIGHT_IMAGES')) { 1289 $char = intval(($bwidth - 40) / 6.5); 1290 } else { 1291 $char = ($bwidth / 6.5); 1292 } 1293 if ($this->canShowName()) { 1294 $tmp = $this->getAllNames(); 1295 $givn = $tmp[$this->getPrimaryName()]['givn']; 1296 $surn = $tmp[$this->getPrimaryName()]['surname']; 1297 $new_givn = explode(' ', $givn); 1298 $count_givn = count($new_givn); 1299 $len_givn = mb_strlen($givn); 1300 $len_surn = mb_strlen($surn); 1301 $len = $len_givn + $len_surn; 1302 $i = 1; 1303 while ($len > $char && $i <= $count_givn) { 1304 $new_givn[$count_givn - $i] = mb_substr($new_givn[$count_givn - $i], 0, 1); 1305 $givn = implode(' ', $new_givn); 1306 $len_givn = mb_strlen($givn); 1307 $len = $len_givn + $len_surn; 1308 $i++; 1309 } 1310 $max_surn = $char - $i * 2; 1311 if ($len_surn > $max_surn) { 1312 $surn = substr($surn, 0, $max_surn) . '…'; 1313 } 1314 $shortname = str_replace( 1315 array('@P.N.', '@N.N.'), 1316 array($UNKNOWN_PN, $UNKNOWN_NN), 1317 $givn . ' ' . $surn 1318 ); 1319 1320 return $shortname; 1321 } else { 1322 return I18N::translate('Private'); 1323 } 1324 } 1325} 1326