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