xref: /webtrees/app/Fact.php (revision d53324c994737e50e57b9e0bc691b4f742b400b2)
1a25f0a04SGreg Roach<?php
2a25f0a04SGreg Roach/**
3a25f0a04SGreg Roach * webtrees: online genealogy
46bdf7674SGreg Roach * Copyright (C) 2017 webtrees development team
5a25f0a04SGreg Roach * This program is free software: you can redistribute it and/or modify
6a25f0a04SGreg Roach * it under the terms of the GNU General Public License as published by
7a25f0a04SGreg Roach * the Free Software Foundation, either version 3 of the License, or
8a25f0a04SGreg Roach * (at your option) any later version.
9a25f0a04SGreg Roach * This program is distributed in the hope that it will be useful,
10a25f0a04SGreg Roach * but WITHOUT ANY WARRANTY; without even the implied warranty of
11a25f0a04SGreg Roach * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12a25f0a04SGreg Roach * GNU General Public License for more details.
13a25f0a04SGreg Roach * You should have received a copy of the GNU General Public License
14a25f0a04SGreg Roach * along with this program. If not, see <http://www.gnu.org/licenses/>.
15a25f0a04SGreg Roach */
1676692c8bSGreg Roachnamespace Fisharebest\Webtrees;
1776692c8bSGreg Roach
183d7a8a4cSGreg Roachuse Fisharebest\Webtrees\Functions\FunctionsPrint;
198f5f5da8SGreg Roachuse InvalidArgumentException;
20a25f0a04SGreg Roach
21a25f0a04SGreg Roach/**
2276692c8bSGreg Roach * A GEDCOM fact or event object.
23a25f0a04SGreg Roach */
24a25f0a04SGreg Roachclass Fact {
25a25f0a04SGreg Roach	/** @var string Unique identifier for this fact (currently implemented as a hash of the raw data). */
26a25f0a04SGreg Roach	private $fact_id;
27a25f0a04SGreg Roach
28a25f0a04SGreg Roach	/** @var GedcomRecord The GEDCOM record from which this fact is taken */
29a25f0a04SGreg Roach	private $parent;
30a25f0a04SGreg Roach
31a25f0a04SGreg Roach	/** @var string The raw GEDCOM data for this fact */
32a25f0a04SGreg Roach	private $gedcom;
33a25f0a04SGreg Roach
34a25f0a04SGreg Roach	/** @var string The GEDCOM tag for this record */
35a25f0a04SGreg Roach	private $tag;
36a25f0a04SGreg Roach
37cbc1590aSGreg Roach	/** @var bool Is this a recently deleted fact, pending approval? */
38a25f0a04SGreg Roach	private $pending_deletion = false;
39a25f0a04SGreg Roach
40cbc1590aSGreg Roach	/** @var bool Is this a recently added fact, pending approval? */
41a25f0a04SGreg Roach	private $pending_addition = false;
42a25f0a04SGreg Roach
43a25f0a04SGreg Roach	/** @var Date The date of this fact, from the “2 DATE …” attribute */
44a25f0a04SGreg Roach	private $date;
45a25f0a04SGreg Roach
46a25f0a04SGreg Roach	/** @var Place The place of this fact, from the “2 PLAC …” attribute */
47a25f0a04SGreg Roach	private $place;
48a25f0a04SGreg Roach
493d7a8a4cSGreg Roach	/** @var int Temporary(!) variable Used by Functions::sortFacts() */
50a25f0a04SGreg Roach	public $sortOrder;
51a25f0a04SGreg Roach
52a25f0a04SGreg Roach	/**
53a25f0a04SGreg Roach	 * Create an event object from a gedcom fragment.
54a25f0a04SGreg Roach	 * We need the parent object (to check privacy) and a (pseudo) fact ID to
55a25f0a04SGreg Roach	 * identify the fact within the record.
56a25f0a04SGreg Roach	 *
57a25f0a04SGreg Roach	 * @param string       $gedcom
58a25f0a04SGreg Roach	 * @param GedcomRecord $parent
59a25f0a04SGreg Roach	 * @param string       $fact_id
60a25f0a04SGreg Roach	 *
618f5f5da8SGreg Roach	 * @throws InvalidArgumentException
62a25f0a04SGreg Roach	 */
63a25f0a04SGreg Roach	public function __construct($gedcom, GedcomRecord $parent, $fact_id) {
64a25f0a04SGreg Roach		if (preg_match('/^1 (' . WT_REGEX_TAG . ')/', $gedcom, $match)) {
65a25f0a04SGreg Roach			$this->gedcom  = $gedcom;
66a25f0a04SGreg Roach			$this->parent  = $parent;
67a25f0a04SGreg Roach			$this->fact_id = $fact_id;
68a25f0a04SGreg Roach			$this->tag     = $match[1];
69a25f0a04SGreg Roach		} else {
708f5f5da8SGreg Roach			throw new InvalidArgumentException('Invalid GEDCOM data passed to Fact::_construct(' . $gedcom . ')');
71a25f0a04SGreg Roach		}
72a25f0a04SGreg Roach	}
73a25f0a04SGreg Roach
74a25f0a04SGreg Roach	/**
75a25f0a04SGreg Roach	 * Get the value of level 1 data in the fact
76a25f0a04SGreg Roach	 * Allow for multi-line values
77a25f0a04SGreg Roach	 *
78baacc364SGreg Roach	 * @return string
79a25f0a04SGreg Roach	 */
80a25f0a04SGreg Roach	public function getValue() {
81a25f0a04SGreg Roach		if (preg_match('/^1 (?:' . $this->tag . ') ?(.*(?:(?:\n2 CONT ?.*)*))/', $this->gedcom, $match)) {
82a25f0a04SGreg Roach			return preg_replace("/\n2 CONT ?/", "\n", $match[1]);
83a25f0a04SGreg Roach		} else {
84baacc364SGreg Roach			return '';
85a25f0a04SGreg Roach		}
86a25f0a04SGreg Roach	}
87a25f0a04SGreg Roach
88a25f0a04SGreg Roach	/**
89a25f0a04SGreg Roach	 * Get the record to which this fact links
90a25f0a04SGreg Roach	 *
91805a90eaSGreg Roach	 * @return Individual|Family|Source|Repository|Media|Note|null
92a25f0a04SGreg Roach	 */
93a25f0a04SGreg Roach	public function getTarget() {
94a25f0a04SGreg Roach		$xref = trim($this->getValue(), '@');
95a25f0a04SGreg Roach		switch ($this->tag) {
96a25f0a04SGreg Roach			case 'FAMC':
97a25f0a04SGreg Roach			case 'FAMS':
9824ec66ceSGreg Roach				return Family::getInstance($xref, $this->getParent()->getTree());
99a25f0a04SGreg Roach			case 'HUSB':
100a25f0a04SGreg Roach			case 'WIFE':
101a25f0a04SGreg Roach			case 'CHIL':
10224ec66ceSGreg Roach				return Individual::getInstance($xref, $this->getParent()->getTree());
103a25f0a04SGreg Roach			case 'SOUR':
10424ec66ceSGreg Roach				return Source::getInstance($xref, $this->getParent()->getTree());
105a25f0a04SGreg Roach			case 'OBJE':
10624ec66ceSGreg Roach				return Media::getInstance($xref, $this->getParent()->getTree());
107a25f0a04SGreg Roach			case 'REPO':
10824ec66ceSGreg Roach				return Repository::getInstance($xref, $this->getParent()->getTree());
109a25f0a04SGreg Roach			case 'NOTE':
11024ec66ceSGreg Roach				return Note::getInstance($xref, $this->getParent()->getTree());
111a25f0a04SGreg Roach			default:
11224ec66ceSGreg Roach				return GedcomRecord::getInstance($xref, $this->getParent()->getTree());
113a25f0a04SGreg Roach		}
114a25f0a04SGreg Roach	}
115a25f0a04SGreg Roach
116a25f0a04SGreg Roach	/**
117a25f0a04SGreg Roach	 * Get the value of level 2 data in the fact
118a25f0a04SGreg Roach	 *
119a25f0a04SGreg Roach	 * @param string $tag
120a25f0a04SGreg Roach	 *
121baacc364SGreg Roach	 * @return string
122a25f0a04SGreg Roach	 */
123a25f0a04SGreg Roach	public function getAttribute($tag) {
124a25f0a04SGreg Roach		if (preg_match('/\n2 (?:' . $tag . ') ?(.*(?:(?:\n3 CONT ?.*)*)*)/', $this->gedcom, $match)) {
125a25f0a04SGreg Roach			return preg_replace("/\n3 CONT ?/", "\n", $match[1]);
126a25f0a04SGreg Roach		} else {
127baacc364SGreg Roach			return '';
128a25f0a04SGreg Roach		}
129a25f0a04SGreg Roach	}
130a25f0a04SGreg Roach
131a25f0a04SGreg Roach	/**
132a25f0a04SGreg Roach	 * Do the privacy rules allow us to display this fact to the current user
133a25f0a04SGreg Roach	 *
134cbc1590aSGreg Roach	 * @param int|null $access_level
135a25f0a04SGreg Roach	 *
136cbc1590aSGreg Roach	 * @return bool
137a25f0a04SGreg Roach	 */
1384b9ff166SGreg Roach	public function canShow($access_level = null) {
1394b9ff166SGreg Roach		if ($access_level === null) {
1404b9ff166SGreg Roach			$access_level = Auth::accessLevel($this->getParent()->getTree());
1414b9ff166SGreg Roach		}
1424b9ff166SGreg Roach
143a25f0a04SGreg Roach		// Does this record have an explicit RESN?
144a25f0a04SGreg Roach		if (strpos($this->gedcom, "\n2 RESN confidential")) {
1454b9ff166SGreg Roach			return Auth::PRIV_NONE >= $access_level;
146a25f0a04SGreg Roach		}
147a25f0a04SGreg Roach		if (strpos($this->gedcom, "\n2 RESN privacy")) {
1484b9ff166SGreg Roach			return Auth::PRIV_USER >= $access_level;
149a25f0a04SGreg Roach		}
150a25f0a04SGreg Roach		if (strpos($this->gedcom, "\n2 RESN none")) {
151a25f0a04SGreg Roach			return true;
152a25f0a04SGreg Roach		}
153a25f0a04SGreg Roach
154a25f0a04SGreg Roach		// Does this record have a default RESN?
155a25f0a04SGreg Roach		$xref                    = $this->parent->getXref();
156518bbdc1SGreg Roach		$fact_privacy            = $this->parent->getTree()->getFactPrivacy();
157518bbdc1SGreg Roach		$individual_fact_privacy = $this->parent->getTree()->getIndividualFactPrivacy();
158518bbdc1SGreg Roach		if (isset($individual_fact_privacy[$xref][$this->tag])) {
159518bbdc1SGreg Roach			return $individual_fact_privacy[$xref][$this->tag] >= $access_level;
160a25f0a04SGreg Roach		}
161518bbdc1SGreg Roach		if (isset($fact_privacy[$this->tag])) {
162518bbdc1SGreg Roach			return $fact_privacy[$this->tag] >= $access_level;
163a25f0a04SGreg Roach		}
164a25f0a04SGreg Roach
165a25f0a04SGreg Roach		// No restrictions - it must be public
166a25f0a04SGreg Roach		return true;
167a25f0a04SGreg Roach	}
168a25f0a04SGreg Roach
169a25f0a04SGreg Roach	/**
170a25f0a04SGreg Roach	 * Check whether this fact is protected against edit
171a25f0a04SGreg Roach	 *
172cbc1590aSGreg Roach	 * @return bool
173a25f0a04SGreg Roach	 */
174a25f0a04SGreg Roach	public function canEdit() {
175a25f0a04SGreg Roach		// Managers can edit anything
176a25f0a04SGreg Roach		// Members cannot edit RESN, CHAN and locked records
177a25f0a04SGreg Roach		return
178a25f0a04SGreg Roach			$this->parent->canEdit() && !$this->isPendingDeletion() && (
1794b9ff166SGreg Roach				Auth::isManager($this->parent->getTree()) ||
1804b9ff166SGreg Roach				Auth::isEditor($this->parent->getTree()) && strpos($this->gedcom, "\n2 RESN locked") === false && $this->getTag() != 'RESN' && $this->getTag() != 'CHAN'
181a25f0a04SGreg Roach			);
182a25f0a04SGreg Roach	}
183a25f0a04SGreg Roach
184a25f0a04SGreg Roach	/**
185a25f0a04SGreg Roach	 * The place where the event occured.
186a25f0a04SGreg Roach	 *
187a25f0a04SGreg Roach	 * @return Place
188a25f0a04SGreg Roach	 */
189a25f0a04SGreg Roach	public function getPlace() {
190a25f0a04SGreg Roach		if ($this->place === null) {
19184caa210SGreg Roach			$this->place = new Place($this->getAttribute('PLAC'), $this->getParent()->getTree());
192a25f0a04SGreg Roach		}
193a25f0a04SGreg Roach
194a25f0a04SGreg Roach		return $this->place;
195a25f0a04SGreg Roach	}
196a25f0a04SGreg Roach
197a25f0a04SGreg Roach	/**
198a25f0a04SGreg Roach	 * Get the date for this fact.
199a25f0a04SGreg Roach	 * We can call this function many times, especially when sorting,
200a25f0a04SGreg Roach	 * so keep a copy of the date.
201a25f0a04SGreg Roach	 *
202a25f0a04SGreg Roach	 * @return Date
203a25f0a04SGreg Roach	 */
204a25f0a04SGreg Roach	public function getDate() {
205a25f0a04SGreg Roach		if ($this->date === null) {
206a25f0a04SGreg Roach			$this->date = new Date($this->getAttribute('DATE'));
207a25f0a04SGreg Roach		}
208a25f0a04SGreg Roach
209a25f0a04SGreg Roach		return $this->date;
210a25f0a04SGreg Roach	}
211a25f0a04SGreg Roach
212a25f0a04SGreg Roach	/**
213a25f0a04SGreg Roach	 * The raw GEDCOM data for this fact
214a25f0a04SGreg Roach	 *
215a25f0a04SGreg Roach	 * @return string
216a25f0a04SGreg Roach	 */
217a25f0a04SGreg Roach	public function getGedcom() {
218a25f0a04SGreg Roach		return $this->gedcom;
219a25f0a04SGreg Roach	}
220a25f0a04SGreg Roach
221a25f0a04SGreg Roach	/**
222a25f0a04SGreg Roach	 * Get a (pseudo) primary key for this fact.
223a25f0a04SGreg Roach	 *
224a25f0a04SGreg Roach	 * @return string
225a25f0a04SGreg Roach	 */
226a25f0a04SGreg Roach	public function getFactId() {
227a25f0a04SGreg Roach		return $this->fact_id;
228a25f0a04SGreg Roach	}
229a25f0a04SGreg Roach
230a25f0a04SGreg Roach	// What sort of fact is this?
231a25f0a04SGreg Roach	/**
232a25f0a04SGreg Roach	 * What is the tag (type) of this fact, such as BIRT, MARR or DEAT.
233a25f0a04SGreg Roach	 *
234a25f0a04SGreg Roach	 * @return string
235a25f0a04SGreg Roach	 */
236a25f0a04SGreg Roach	public function getTag() {
237a25f0a04SGreg Roach		return $this->tag;
238a25f0a04SGreg Roach	}
239a25f0a04SGreg Roach
240a25f0a04SGreg Roach	/**
241a25f0a04SGreg Roach	 * Used to convert a real fact (e.g. BIRT) into a close-relative’s fact (e.g. _BIRT_CHIL)
242a25f0a04SGreg Roach	 *
243a25f0a04SGreg Roach	 * @param string $tag
244a25f0a04SGreg Roach	 */
245a25f0a04SGreg Roach	public function setTag($tag) {
246a25f0a04SGreg Roach		$this->tag = $tag;
247a25f0a04SGreg Roach	}
248a25f0a04SGreg Roach
249a25f0a04SGreg Roach	//
250a25f0a04SGreg Roach	/**
251a25f0a04SGreg Roach	 * The Person/Family record where this Fact came from
252a25f0a04SGreg Roach	 *
2536e59b7c4SGreg Roach	 * @return Individual|Family|Source|Repository|Media|Note|GedcomRecord
254a25f0a04SGreg Roach	 */
255a25f0a04SGreg Roach	public function getParent() {
256a25f0a04SGreg Roach		return $this->parent;
257a25f0a04SGreg Roach	}
258a25f0a04SGreg Roach
259a25f0a04SGreg Roach	/**
260a25f0a04SGreg Roach	 * Get the name of this fact type, for use as a label.
261a25f0a04SGreg Roach	 *
262a25f0a04SGreg Roach	 * @return string
263a25f0a04SGreg Roach	 */
264a25f0a04SGreg Roach	public function getLabel() {
265a25f0a04SGreg Roach		switch ($this->tag) {
266a25f0a04SGreg Roach			case 'EVEN':
267a25f0a04SGreg Roach			case 'FACT':
268baacc364SGreg Roach				if ($this->getAttribute('TYPE') !== '') {
269a25f0a04SGreg Roach					// Custom FACT/EVEN - with a TYPE
270*d53324c9SGreg Roach					return I18N::translate(e($this->getAttribute('TYPE')));
271a25f0a04SGreg Roach				}
272a25f0a04SGreg Roach				// no break - drop into next case
273a25f0a04SGreg Roach			default:
274764a01d9SGreg Roach				return GedcomTag::getLabel($this->tag, $this->parent);
275a25f0a04SGreg Roach		}
276a25f0a04SGreg Roach	}
277a25f0a04SGreg Roach
278a25f0a04SGreg Roach	/**
279a25f0a04SGreg Roach	 * This is a newly deleted fact, pending approval.
280a25f0a04SGreg Roach	 */
281a25f0a04SGreg Roach	public function setPendingDeletion() {
282a25f0a04SGreg Roach		$this->pending_deletion = true;
283a25f0a04SGreg Roach		$this->pending_addition = false;
284a25f0a04SGreg Roach	}
285a25f0a04SGreg Roach
286a25f0a04SGreg Roach	/**
287a25f0a04SGreg Roach	 * Is this a newly deleted fact, pending approval.
288a25f0a04SGreg Roach	 *
289cbc1590aSGreg Roach	 * @return bool
290a25f0a04SGreg Roach	 */
291a25f0a04SGreg Roach	public function isPendingDeletion() {
292a25f0a04SGreg Roach		return $this->pending_deletion;
293a25f0a04SGreg Roach	}
294a25f0a04SGreg Roach
295a25f0a04SGreg Roach	/**
296a25f0a04SGreg Roach	 * This is a newly added fact, pending approval.
297a25f0a04SGreg Roach	 */
298a25f0a04SGreg Roach	public function setPendingAddition() {
299a25f0a04SGreg Roach		$this->pending_addition = true;
300a25f0a04SGreg Roach		$this->pending_deletion = false;
301a25f0a04SGreg Roach	}
302a25f0a04SGreg Roach
303a25f0a04SGreg Roach	/**
304a25f0a04SGreg Roach	 * Is this a newly added fact, pending approval.
305a25f0a04SGreg Roach	 *
306cbc1590aSGreg Roach	 * @return bool
307a25f0a04SGreg Roach	 */
308a25f0a04SGreg Roach	public function isPendingAddition() {
309a25f0a04SGreg Roach		return $this->pending_addition;
310a25f0a04SGreg Roach	}
311a25f0a04SGreg Roach
312a25f0a04SGreg Roach	/**
313a25f0a04SGreg Roach	 * Source citations linked to this fact
314a25f0a04SGreg Roach	 *
315a25f0a04SGreg Roach	 * @return string[]
316a25f0a04SGreg Roach	 */
317a25f0a04SGreg Roach	public function getCitations() {
318a25f0a04SGreg Roach		preg_match_all('/\n(2 SOUR @(' . WT_REGEX_XREF . ')@(?:\n[3-9] .*)*)/', $this->getGedcom(), $matches, PREG_SET_ORDER);
31913abd6f3SGreg Roach		$citations = [];
320a25f0a04SGreg Roach		foreach ($matches as $match) {
32124ec66ceSGreg Roach			$source = Source::getInstance($match[2], $this->getParent()->getTree());
322a25f0a04SGreg Roach			if ($source->canShow()) {
323a25f0a04SGreg Roach				$citations[] = $match[1];
324a25f0a04SGreg Roach			}
325a25f0a04SGreg Roach		}
326a25f0a04SGreg Roach
327a25f0a04SGreg Roach		return $citations;
328a25f0a04SGreg Roach	}
329a25f0a04SGreg Roach
330a25f0a04SGreg Roach	/**
331a25f0a04SGreg Roach	 * Notes (inline and objects) linked to this fact
332a25f0a04SGreg Roach	 *
333a25f0a04SGreg Roach	 * @return string[]|Note[]
334a25f0a04SGreg Roach	 */
335a25f0a04SGreg Roach	public function getNotes() {
33613abd6f3SGreg Roach		$notes = [];
337a25f0a04SGreg Roach		preg_match_all('/\n2 NOTE ?(.*(?:\n3.*)*)/', $this->getGedcom(), $matches);
338a25f0a04SGreg Roach		foreach ($matches[1] as $match) {
339a25f0a04SGreg Roach			$note = preg_replace("/\n3 CONT ?/", "\n", $match);
340a25f0a04SGreg Roach			if (preg_match('/@(' . WT_REGEX_XREF . ')@/', $note, $nmatch)) {
34124ec66ceSGreg Roach				$note = Note::getInstance($nmatch[1], $this->getParent()->getTree());
342a25f0a04SGreg Roach				if ($note && $note->canShow()) {
343a25f0a04SGreg Roach					// A note object
344a25f0a04SGreg Roach					$notes[] = $note;
345a25f0a04SGreg Roach				}
346a25f0a04SGreg Roach			} else {
347a25f0a04SGreg Roach				// An inline note
348a25f0a04SGreg Roach				$notes[] = $note;
349a25f0a04SGreg Roach			}
350a25f0a04SGreg Roach		}
351a25f0a04SGreg Roach
352a25f0a04SGreg Roach		return $notes;
353a25f0a04SGreg Roach	}
354a25f0a04SGreg Roach
355a25f0a04SGreg Roach	/**
356a25f0a04SGreg Roach	 * Media objects linked to this fact
357a25f0a04SGreg Roach	 *
358a25f0a04SGreg Roach	 * @return Media[]
359a25f0a04SGreg Roach	 */
360a25f0a04SGreg Roach	public function getMedia() {
36113abd6f3SGreg Roach		$media = [];
362a25f0a04SGreg Roach		preg_match_all('/\n2 OBJE @(' . WT_REGEX_XREF . ')@/', $this->getGedcom(), $matches);
363a25f0a04SGreg Roach		foreach ($matches[1] as $match) {
36424ec66ceSGreg Roach			$obje = Media::getInstance($match, $this->getParent()->getTree());
365a25f0a04SGreg Roach			if ($obje->canShow()) {
366a25f0a04SGreg Roach				$media[] = $obje;
367a25f0a04SGreg Roach			}
368a25f0a04SGreg Roach		}
369a25f0a04SGreg Roach
370a25f0a04SGreg Roach		return $media;
371a25f0a04SGreg Roach	}
372a25f0a04SGreg Roach
373a25f0a04SGreg Roach	/**
374a25f0a04SGreg Roach	 * A one-line summary of the fact - for charts, etc.
375a25f0a04SGreg Roach	 *
376a25f0a04SGreg Roach	 * @return string
377a25f0a04SGreg Roach	 */
378a25f0a04SGreg Roach	public function summary() {
37913abd6f3SGreg Roach		$attributes = [];
380a25f0a04SGreg Roach		$target     = $this->getTarget();
381a25f0a04SGreg Roach		if ($target) {
382a25f0a04SGreg Roach			$attributes[] = $target->getFullName();
383a25f0a04SGreg Roach		} else {
384726d7b07SGreg Roach			// Fact value
385a25f0a04SGreg Roach			$value = $this->getValue();
386726d7b07SGreg Roach			if ($value !== '' && $value !== 'Y') {
387*d53324c9SGreg Roach				$attributes[] = '<span dir="auto">' . e($value) . '</span>';
388a25f0a04SGreg Roach			}
389726d7b07SGreg Roach			// Fact date
390a25f0a04SGreg Roach			$date = $this->getDate();
391726d7b07SGreg Roach			if ($date->isOK()) {
392af459ec4SGreg Roach				if (in_array($this->getTag(), explode('|', WT_EVENTS_BIRT)) && $this->getParent() instanceof Individual && $this->getParent()->getTree()->getPreference('SHOW_PARENTS_AGE')) {
3933d7a8a4cSGreg Roach					$attributes[] = $date->display() . FunctionsPrint::formatParentsAges($this->getParent(), $date);
394a25f0a04SGreg Roach				} else {
395a25f0a04SGreg Roach					$attributes[] = $date->display();
396a25f0a04SGreg Roach				}
397726d7b07SGreg Roach			}
398726d7b07SGreg Roach			// Fact place
399726d7b07SGreg Roach			if (!$this->getPlace()->isEmpty()) {
400726d7b07SGreg Roach				$attributes[] = $this->getPlace()->getShortName();
401a25f0a04SGreg Roach			}
402a25f0a04SGreg Roach		}
4032d8f08abSGreg Roach
4042d8f08abSGreg Roach		$class = 'fact_' . $this->getTag();
405a25f0a04SGreg Roach		if ($this->isPendingAddition()) {
4062d8f08abSGreg Roach			$class .= ' new';
407a25f0a04SGreg Roach		} elseif ($this->isPendingDeletion()) {
4082d8f08abSGreg Roach			$class .= ' old';
409a25f0a04SGreg Roach		}
4102d8f08abSGreg Roach
4112d8f08abSGreg Roach		return
4122d8f08abSGreg Roach			'<div class="' . $class . '">' .
4132d8f08abSGreg Roach			/* I18N: a label/value pair, such as “Occupation: Farmer”. Some languages may need to change the punctuation. */
4142d8f08abSGreg Roach			I18N::translate('<span class="label">%1$s:</span> <span class="field" dir="auto">%2$s</span>', $this->getLabel(), implode(' — ', $attributes)) .
4152d8f08abSGreg Roach			'</div>';
416a25f0a04SGreg Roach	}
417a25f0a04SGreg Roach
418a25f0a04SGreg Roach	/**
419a25f0a04SGreg Roach	 * Static Helper functions to sort events
420a25f0a04SGreg Roach	 *
421a25f0a04SGreg Roach	 * @param Fact $a Fact one
422a25f0a04SGreg Roach	 * @param Fact $b Fact two
423a25f0a04SGreg Roach	 *
424cbc1590aSGreg Roach	 * @return int
425a25f0a04SGreg Roach	 */
426a25f0a04SGreg Roach	public static function compareDate(Fact $a, Fact $b) {
427a25f0a04SGreg Roach		if ($a->getDate()->isOK() && $b->getDate()->isOK()) {
428a25f0a04SGreg Roach			// If both events have dates, compare by date
429f5b60decSGreg Roach			$ret = Date::compare($a->getDate(), $b->getDate());
430a25f0a04SGreg Roach
431a25f0a04SGreg Roach			if ($ret == 0) {
432a25f0a04SGreg Roach				// If dates are the same, compare by fact type
433a25f0a04SGreg Roach				$ret = self::compareType($a, $b);
434a25f0a04SGreg Roach
435a25f0a04SGreg Roach				// If the fact type is also the same, retain the initial order
436a25f0a04SGreg Roach				if ($ret == 0) {
437a25f0a04SGreg Roach					$ret = $a->sortOrder - $b->sortOrder;
438a25f0a04SGreg Roach				}
439a25f0a04SGreg Roach			}
440a25f0a04SGreg Roach
441a25f0a04SGreg Roach			return $ret;
442a25f0a04SGreg Roach		} else {
443a25f0a04SGreg Roach			// One or both events have no date - retain the initial order
444a25f0a04SGreg Roach			return $a->sortOrder - $b->sortOrder;
445a25f0a04SGreg Roach		}
446a25f0a04SGreg Roach	}
447a25f0a04SGreg Roach
448a25f0a04SGreg Roach	/**
449a25f0a04SGreg Roach	 * Static method to compare two events by their type.
450a25f0a04SGreg Roach	 *
451a25f0a04SGreg Roach	 * @param Fact $a Fact one
452a25f0a04SGreg Roach	 * @param Fact $b Fact two
453a25f0a04SGreg Roach	 *
454cbc1590aSGreg Roach	 * @return int
455a25f0a04SGreg Roach	 */
456a25f0a04SGreg Roach	public static function compareType(Fact $a, Fact $b) {
457a25f0a04SGreg Roach		global $factsort;
458a25f0a04SGreg Roach
459a25f0a04SGreg Roach		if (empty($factsort)) {
460a25f0a04SGreg Roach			$factsort = array_flip(
46113abd6f3SGreg Roach				[
462a25f0a04SGreg Roach					'BIRT',
463a25f0a04SGreg Roach					'_HNM',
464a25f0a04SGreg Roach					'ALIA', '_AKA', '_AKAN',
465a25f0a04SGreg Roach					'ADOP', '_ADPF', '_ADPF',
466a25f0a04SGreg Roach					'_BRTM',
467a25f0a04SGreg Roach					'CHR', 'BAPM',
468a25f0a04SGreg Roach					'FCOM',
469a25f0a04SGreg Roach					'CONF',
470a25f0a04SGreg Roach					'BARM', 'BASM',
471a25f0a04SGreg Roach					'EDUC',
472a25f0a04SGreg Roach					'GRAD',
473a25f0a04SGreg Roach					'_DEG',
474a25f0a04SGreg Roach					'EMIG', 'IMMI',
475a25f0a04SGreg Roach					'NATU',
476a25f0a04SGreg Roach					'_MILI', '_MILT',
477a25f0a04SGreg Roach					'ENGA',
478a25f0a04SGreg Roach					'MARB', 'MARC', 'MARL', '_MARI', '_MBON',
479a25f0a04SGreg Roach					'MARR', 'MARR_CIVIL', 'MARR_RELIGIOUS', 'MARR_PARTNERS', 'MARR_UNKNOWN', '_COML',
480a25f0a04SGreg Roach					'_STAT',
481a25f0a04SGreg Roach					'_SEPR',
482a25f0a04SGreg Roach					'DIVF',
483a25f0a04SGreg Roach					'MARS',
484a25f0a04SGreg Roach					'_BIRT_CHIL',
485a25f0a04SGreg Roach					'DIV', 'ANUL',
486a25f0a04SGreg Roach					'_BIRT_', '_MARR_', '_DEAT_', '_BURI_', // other events of close relatives
487a25f0a04SGreg Roach					'CENS',
488a25f0a04SGreg Roach					'OCCU',
489a25f0a04SGreg Roach					'RESI',
490a25f0a04SGreg Roach					'PROP',
491a25f0a04SGreg Roach					'CHRA',
492a25f0a04SGreg Roach					'RETI',
493a25f0a04SGreg Roach					'FACT', 'EVEN',
494a25f0a04SGreg Roach					'_NMR', '_NMAR', 'NMR',
495a25f0a04SGreg Roach					'NCHI',
496a25f0a04SGreg Roach					'WILL',
497a25f0a04SGreg Roach					'_HOL',
498a25f0a04SGreg Roach					'_????_',
499a25f0a04SGreg Roach					'DEAT',
500a25f0a04SGreg Roach					'_FNRL', 'CREM', 'BURI', '_INTE',
501a25f0a04SGreg Roach					'_YART',
502a25f0a04SGreg Roach					'_NLIV',
503a25f0a04SGreg Roach					'PROB',
504a25f0a04SGreg Roach					'TITL',
505a25f0a04SGreg Roach					'COMM',
506a25f0a04SGreg Roach					'NATI',
507a25f0a04SGreg Roach					'CITN',
508a25f0a04SGreg Roach					'CAST',
509a25f0a04SGreg Roach					'RELI',
510a25f0a04SGreg Roach					'SSN', 'IDNO',
511a25f0a04SGreg Roach					'TEMP',
512a25f0a04SGreg Roach					'SLGC', 'BAPL', 'CONL', 'ENDL', 'SLGS',
513a25f0a04SGreg Roach					'ADDR', 'PHON', 'EMAIL', '_EMAIL', 'EMAL', 'FAX', 'WWW', 'URL', '_URL',
514a25f0a04SGreg Roach					'FILE', // For media objects
515a25f0a04SGreg Roach					'AFN', 'REFN', '_PRMN', 'REF', 'RIN', '_UID',
516a25f0a04SGreg Roach					'OBJE', 'NOTE', 'SOUR',
517a25f0a04SGreg Roach					'CHAN', '_TODO',
51813abd6f3SGreg Roach				]
519a25f0a04SGreg Roach			);
520a25f0a04SGreg Roach		}
521a25f0a04SGreg Roach
522a25f0a04SGreg Roach		// Facts from same families stay grouped together
523a25f0a04SGreg Roach		// Keep MARR and DIV from the same families from mixing with events from other FAMs
524a25f0a04SGreg Roach		// Use the original order in which the facts were added
525a25f0a04SGreg Roach		if ($a->parent instanceof Family && $b->parent instanceof Family && $a->parent !== $b->parent) {
526a25f0a04SGreg Roach			return $a->sortOrder - $b->sortOrder;
527a25f0a04SGreg Roach		}
528a25f0a04SGreg Roach
529a25f0a04SGreg Roach		$atag = $a->getTag();
530a25f0a04SGreg Roach		$btag = $b->getTag();
531a25f0a04SGreg Roach
532a25f0a04SGreg Roach		// Events not in the above list get mapped onto one that is.
533a25f0a04SGreg Roach		if (!array_key_exists($atag, $factsort)) {
534a25f0a04SGreg Roach			if (preg_match('/^(_(BIRT|MARR|DEAT|BURI)_)/', $atag, $match)) {
535a25f0a04SGreg Roach				$atag = $match[1];
536a25f0a04SGreg Roach			} else {
5377a6ee1acSGreg Roach				$atag = '_????_';
538a25f0a04SGreg Roach			}
539a25f0a04SGreg Roach		}
540a25f0a04SGreg Roach
541a25f0a04SGreg Roach		if (!array_key_exists($btag, $factsort)) {
542a25f0a04SGreg Roach			if (preg_match('/^(_(BIRT|MARR|DEAT|BURI)_)/', $btag, $match)) {
543a25f0a04SGreg Roach				$btag = $match[1];
544a25f0a04SGreg Roach			} else {
5457a6ee1acSGreg Roach				$btag = '_????_';
546a25f0a04SGreg Roach			}
547a25f0a04SGreg Roach		}
548a25f0a04SGreg Roach
549a25f0a04SGreg Roach		// - Don't let dated after DEAT/BURI facts sort non-dated facts before DEAT/BURI
550a25f0a04SGreg Roach		// - Treat dated after BURI facts as BURI instead
551baacc364SGreg Roach		if ($a->getAttribute('DATE') !== '' && $factsort[$atag] > $factsort['BURI'] && $factsort[$atag] < $factsort['CHAN']) {
552a25f0a04SGreg Roach			$atag = 'BURI';
553a25f0a04SGreg Roach		}
554a25f0a04SGreg Roach
555baacc364SGreg Roach		if ($b->getAttribute('DATE') !== '' && $factsort[$btag] > $factsort['BURI'] && $factsort[$btag] < $factsort['CHAN']) {
556a25f0a04SGreg Roach			$btag = 'BURI';
557a25f0a04SGreg Roach		}
558a25f0a04SGreg Roach
559a25f0a04SGreg Roach		$ret = $factsort[$atag] - $factsort[$btag];
560a25f0a04SGreg Roach
561a25f0a04SGreg Roach		// If facts are the same then put dated facts before non-dated facts
562a25f0a04SGreg Roach		if ($ret == 0) {
563baacc364SGreg Roach			if ($a->getAttribute('DATE') !== '' && $b->getAttribute('DATE') === '') {
564a25f0a04SGreg Roach				return -1;
565a25f0a04SGreg Roach			}
566a25f0a04SGreg Roach
567baacc364SGreg Roach			if ($b->getAttribute('DATE') !== '' && $a->getAttribute('DATE') === '') {
568a25f0a04SGreg Roach				return 1;
569a25f0a04SGreg Roach			}
570a25f0a04SGreg Roach
571a25f0a04SGreg Roach			// If no sorting preference, then keep original ordering
572a25f0a04SGreg Roach			$ret = $a->sortOrder - $b->sortOrder;
573a25f0a04SGreg Roach		}
574a25f0a04SGreg Roach
575a25f0a04SGreg Roach		return $ret;
576a25f0a04SGreg Roach	}
577a25f0a04SGreg Roach
578a25f0a04SGreg Roach	/**
579a25f0a04SGreg Roach	 * Allow native PHP functions such as array_unique() to work with objects
580a25f0a04SGreg Roach	 *
581a25f0a04SGreg Roach	 * @return string
582a25f0a04SGreg Roach	 */
583a25f0a04SGreg Roach	public function __toString() {
584a25f0a04SGreg Roach		return $this->fact_id . '@' . $this->parent->getXref();
585a25f0a04SGreg Roach	}
586a25f0a04SGreg Roach}
587