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