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