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