1<?php
2/**
3 * webtrees: online genealogy
4 * Copyright (C) 2019 webtrees development team
5 * This program is free software: you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation, either version 3 of the License, or
8 * (at your option) any later version.
9 * This program is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 * GNU General Public License for more details.
13 * You should have received a copy of the GNU General Public License
14 * along with this program. If not, see <http://www.gnu.org/licenses/>.
15 */
16namespace Fisharebest\Webtrees\Functions;
17
18use Fisharebest\Webtrees\Config;
19use Fisharebest\Webtrees\Controller\SearchController;
20use Fisharebest\Webtrees\Date;
21use Fisharebest\Webtrees\Fact;
22use Fisharebest\Webtrees\Family;
23use Fisharebest\Webtrees\Filter;
24use Fisharebest\Webtrees\GedcomCode\GedcomCodeStat;
25use Fisharebest\Webtrees\GedcomCode\GedcomCodeTemp;
26use Fisharebest\Webtrees\GedcomRecord;
27use Fisharebest\Webtrees\GedcomTag;
28use Fisharebest\Webtrees\I18N;
29use Fisharebest\Webtrees\Individual;
30use Fisharebest\Webtrees\Module;
31use Fisharebest\Webtrees\Module\CensusAssistantModule;
32use Fisharebest\Webtrees\Note;
33use Fisharebest\Webtrees\Place;
34use Fisharebest\Webtrees\Session;
35use Fisharebest\Webtrees\Theme;
36use Fisharebest\Webtrees\Tree;
37use Rhumsaa\Uuid\Uuid;
38
39/**
40 * Class FunctionsPrint - common functions
41 */
42class FunctionsPrint
43{
44    /**
45     * print the information for an individual chart box
46     *
47     * find and print a given individuals information for a pedigree chart
48     *
49     * @param Individual $person The person to print
50     * @param int $show_full The style to print the box in, 0 for smaller boxes, 1 for larger boxes
51     */
52    public static function printPedigreePerson(Individual $person = null, $show_full = 1)
53    {
54        switch ($show_full) {
55            case 0:
56                if ($person) {
57                    echo Theme::theme()->individualBoxSmall($person);
58                } else {
59                    echo Theme::theme()->individualBoxSmallEmpty();
60                }
61                break;
62            case 1:
63                if ($person) {
64                    echo Theme::theme()->individualBox($person);
65                } else {
66                    echo Theme::theme()->individualBoxEmpty();
67                }
68                break;
69        }
70    }
71
72    /**
73     * print a note record
74     *
75     * @param string $text
76     * @param int $nlevel the level of the note record
77     * @param string $nrec the note record to print
78     * @param bool $textOnly Don't print the "Note: " introduction
79     *
80     * @return string
81     */
82    public static function printNoteRecord($text, $nlevel, $nrec, $textOnly = false)
83    {
84        global $WT_TREE;
85
86        $text .= Functions::getCont($nlevel, $nrec);
87
88        // Check if shared note (we have already checked that it exists)
89        if (preg_match('/^0 @(' . WT_REGEX_XREF . ')@ NOTE/', $nrec, $match)) {
90            $note  = Note::getInstance($match[1], $WT_TREE);
91            $label = 'SHARED_NOTE';
92            // If Census assistant installed, allow it to format the note
93            if (Module::getModuleByName('GEDFact_assistant')) {
94                $html = CensusAssistantModule::formatCensusNote($note);
95            } else {
96                $html = Filter::formatText($note->getNote(), $WT_TREE);
97            }
98        } else {
99            $note  = null;
100            $label = 'NOTE';
101            $html  = Filter::formatText($text, $WT_TREE);
102        }
103
104        if ($textOnly) {
105            return strip_tags($text);
106        }
107
108        if (strpos($text, "\n") === false) {
109            // A one-line note? strip the block-level tags, so it displays inline
110            return GedcomTag::getLabelValue($label, strip_tags($html, '<a><strong><em>'));
111        } elseif ($WT_TREE->getPreference('EXPAND_NOTES')) {
112            // A multi-line note, and we're expanding notes by default
113            return GedcomTag::getLabelValue($label, $html);
114        } else {
115            // A multi-line note, with an expand/collapse option
116            $element_id = Uuid::uuid4();
117            // NOTE: class "note-details" is (currently) used only by some third-party themes
118            if ($note) {
119                $first_line = '<a href="' . $note->getHtmlUrl() . '">' . $note->getFullName() . '</a>';
120            } else {
121                list($text) = explode("\n", strip_tags($html));
122                $first_line = strlen($text) > 100 ? mb_substr($text, 0, 100) . I18N::translate('…') : $text;
123            }
124
125            return
126                '<div class="fact_NOTE"><span class="label">' .
127                '<a href="#" onclick="expand_layer(\'' . $element_id . '\'); return false;"><i id="' . $element_id . '_img" class="icon-plus"></i></a> ' . GedcomTag::getLabel($label) . ':</span> ' . '<span id="' . $element_id . '-alt">' . $first_line . '</span>' .
128                '</div>' .
129                '<div class="note-details" id="' . $element_id . '" style="display:none">' . $html . '</div>';
130        }
131    }
132
133    /**
134     * Print all of the notes in this fact record
135     *
136     * @param string $factrec The factrecord to print the notes from
137     * @param int $level The level of the factrecord
138     * @param bool $textOnly Don't print the "Note: " introduction
139     *
140     * @return string HTML
141     */
142    public static function printFactNotes($factrec, $level, $textOnly = false)
143    {
144        global $WT_TREE;
145
146        $data          = '';
147        $previous_spos = 0;
148        $nlevel        = $level + 1;
149        $ct            = preg_match_all("/$level NOTE (.*)/", $factrec, $match, PREG_SET_ORDER);
150        for ($j = 0; $j < $ct; $j++) {
151            $spos1 = strpos($factrec, $match[$j][0], $previous_spos);
152            $spos2 = strpos($factrec . "\n$level", "\n$level", $spos1 + 1);
153            if (!$spos2) {
154                $spos2 = strlen($factrec);
155            }
156            $previous_spos = $spos2;
157            $nrec          = substr($factrec, $spos1, $spos2 - $spos1);
158            if (!isset($match[$j][1])) {
159                $match[$j][1] = '';
160            }
161            if (!preg_match('/@(.*)@/', $match[$j][1], $nmatch)) {
162                $data .= self::printNoteRecord($match[$j][1], $nlevel, $nrec, $textOnly);
163            } else {
164                $note = Note::getInstance($nmatch[1], $WT_TREE);
165                if ($note) {
166                    if ($note->canShow()) {
167                        $noterec = $note->getGedcom();
168                        $nt      = preg_match("/0 @$nmatch[1]@ NOTE (.*)/", $noterec, $n1match);
169                        $data .= self::printNoteRecord(($nt > 0) ? $n1match[1] : "", 1, $noterec, $textOnly);
170                        if (!$textOnly) {
171                            if (strpos($noterec, '1 SOUR') !== false) {
172                                $data .= FunctionsPrintFacts::printFactSources($noterec, 1);
173                            }
174                        }
175                    }
176                } else {
177                    $data = '<div class="fact_NOTE"><span class="label">' . I18N::translate('Note') . '</span>: <span class="field error">' . $nmatch[1] . '</span></div>';
178                }
179            }
180            if (!$textOnly) {
181                if (strpos($factrec, "$nlevel SOUR") !== false) {
182                    $data .= "<div class=\"indent\">";
183                    $data .= FunctionsPrintFacts::printFactSources($nrec, $nlevel);
184                    $data .= "</div>";
185                }
186            }
187        }
188
189        return $data;
190    }
191
192    /**
193     * Print a link for a popup help window.
194     *
195     * @param string $help_topic
196     * @param string $module
197     *
198     * @return string
199     */
200    public static function helpLink($help_topic, $module = '')
201    {
202        return '<span class="icon-help" onclick="helpDialog(\'' . $help_topic . '\',\'' . $module . '\'); return false;">&nbsp;</span>';
203    }
204
205    /**
206     * Print an external help link to the wiki site.
207     *
208     * @deprecated - nothing should be so complicated that it needs lengthy instructions!
209     *
210     * @param string $topic
211     *
212     * @return string
213     */
214    public static function wikiHelpLink($topic)
215    {
216        return '<a class="help icon-wiki" href="' . WT_WEBTREES_WIKI . $topic . '" title="' . I18N::translate('webtrees wiki') . '"></a>';
217    }
218
219    /**
220     * When a user has searched for text, highlight any matches in
221     * the displayed string.
222     *
223     * @param string $string
224     *
225     * @return string
226     */
227    public static function highlightSearchHits($string)
228    {
229        global $controller;
230
231        if ($controller instanceof SearchController && $controller->query) {
232            // TODO: when a search contains multiple words, we search independently.
233            // e.g. searching for "FOO BAR" will find records containing both FOO and BAR.
234            // However, we only highlight the original search string, not the search terms.
235            // The controller needs to provide its "query_terms" array.
236            $regex = array();
237            foreach (array($controller->query) as $search_term) {
238                $regex[] = preg_quote($search_term, '/');
239            }
240            // Match these strings, provided they do not occur inside HTML tags
241            $regex = '(' . implode('|', $regex) . ')(?![^<]*>)';
242
243            return preg_replace('/' . $regex . '/i', '<span class="search_hit">$1</span>', $string);
244        } else {
245            return $string;
246        }
247    }
248
249    /**
250     * Format age of parents in HTML
251     *
252     * @param Individual $person child
253     * @param Date $birth_date
254     *
255     * @return string HTML
256     */
257    public static function formatParentsAges(Individual $person, Date $birth_date)
258    {
259        $html     = '';
260        $families = $person->getChildFamilies();
261        // Multiple sets of parents (e.g. adoption) cause complications, so ignore.
262        if ($birth_date->isOK() && count($families) == 1) {
263            $family = current($families);
264            foreach ($family->getSpouses() as $parent) {
265                if ($parent->getBirthDate()->isOK()) {
266                    $sex      = $parent->getSexImage();
267                    $age      = Date::getAge($parent->getBirthDate(), $birth_date, 2);
268                    $deatdate = $parent->getDeathDate();
269                    switch ($parent->getSex()) {
270                        case 'F':
271                            // Highlight mothers who die in childbirth or shortly afterwards
272                            if ($deatdate->isOK() && $deatdate->maximumJulianDay() < $birth_date->minimumJulianDay() + 90) {
273                                $html .= ' <span title="' . GedcomTag::getLabel('_DEAT_PARE', $parent) . '" class="parentdeath">' . $sex . $age . '</span>';
274                            } else {
275                                $html .= ' <span title="' . I18N::translate('Mother’s age') . '">' . $sex . $age . '</span>';
276                            }
277                            break;
278                        case 'M':
279                            // Highlight fathers who die before the birth
280                            if ($deatdate->isOK() && $deatdate->maximumJulianDay() < $birth_date->minimumJulianDay()) {
281                                $html .= ' <span title="' . GedcomTag::getLabel('_DEAT_PARE', $parent) . '" class="parentdeath">' . $sex . $age . '</span>';
282                            } else {
283                                $html .= ' <span title="' . I18N::translate('Father’s age') . '">' . $sex . $age . '</span>';
284                            }
285                            break;
286                        default:
287                            $html .= ' <span title="' . I18N::translate('Parent’s age') . '">' . $sex . $age . '</span>';
288                            break;
289                    }
290                }
291            }
292            if ($html) {
293                $html = '<span class="age">' . $html . '</span>';
294            }
295        }
296
297        return $html;
298    }
299
300    /**
301     * Print fact DATE/TIME
302     *
303     * @param Fact $event event containing the date/age
304     * @param GedcomRecord $record the person (or couple) whose ages should be printed
305     * @param bool $anchor option to print a link to calendar
306     * @param bool $time option to print TIME value
307     *
308     * @return string
309     */
310    public static function formatFactDate(Fact $event, GedcomRecord $record, $anchor, $time)
311    {
312        global $pid;
313
314        $factrec = $event->getGedcom();
315        $html    = '';
316        // Recorded age
317        if (preg_match('/\n2 AGE (.+)/', $factrec, $match)) {
318            $fact_age = $match[1];
319        } else {
320            $fact_age = '';
321        }
322        if (preg_match('/\n2 HUSB\n3 AGE (.+)/', $factrec, $match)) {
323            $husb_age = $match[1];
324        } else {
325            $husb_age = '';
326        }
327        if (preg_match('/\n2 WIFE\n3 AGE (.+)/', $factrec, $match)) {
328            $wife_age = $match[1];
329        } else {
330            $wife_age = '';
331        }
332
333        // Calculated age
334        $fact = $event->getTag();
335        if (preg_match('/\n2 DATE (.+)/', $factrec, $match)) {
336            $date = new Date($match[1]);
337            $html .= ' ' . $date->display($anchor);
338            // time
339            if ($time && preg_match('/\n3 TIME (.+)/', $factrec, $match)) {
340                $html .= ' – <span class="date">' . $match[1] . '</span>';
341            }
342            if ($record instanceof Individual) {
343                if ($fact === 'BIRT' && $record->getTree()->getPreference('SHOW_PARENTS_AGE')) {
344                    // age of parents at child birth
345                    $html .= self::formatParentsAges($record, $date);
346                } elseif ($fact !== 'BIRT' && $fact !== 'CHAN' && $fact !== '_TODO') {
347                    // age at event
348                    $birth_date = $record->getBirthDate();
349                    // Can't use getDeathDate(), as this also gives BURI/CREM events, which
350                    // wouldn't give the correct "days after death" result for people with
351                    // no DEAT.
352                    $death_event = $record->getFirstFact('DEAT');
353                    if ($death_event) {
354                        $death_date = $death_event->getDate();
355                    } else {
356                        $death_date = new Date('');
357                    }
358                    $ageText = '';
359                    if ((Date::compare($date, $death_date) <= 0 || !$record->isDead()) || $fact == 'DEAT') {
360                        // Before death, print age
361                        $age = Date::getAgeGedcom($birth_date, $date);
362                        // Only show calculated age if it differs from recorded age
363                        if ($age != '') {
364                            if (
365                                $fact_age != '' && $fact_age != $age ||
366                                $fact_age == '' && $husb_age == '' && $wife_age == '' ||
367                                $husb_age != '' && $record->getSex() == 'M' && $husb_age != $age ||
368                                $wife_age != '' && $record->getSex() == 'F' && $wife_age != $age
369                            ) {
370                                if ($age != "0d") {
371                                    $ageText = '(' . I18N::translate('Age') . ' ' . FunctionsDate::getAgeAtEvent($age) . ')';
372                                }
373                            }
374                        }
375                    }
376                    if ($fact != 'DEAT' && Date::compare($date, $death_date) >= 0) {
377                        // After death, print time since death
378                        $age = FunctionsDate::getAgeAtEvent(Date::getAgeGedcom($death_date, $date));
379                        if ($age != '') {
380                            if (Date::getAgeGedcom($death_date, $date) == "0d") {
381                                $ageText = '(' . I18N::translate('on the date of death') . ')';
382                            } else {
383                                $ageText = '(' . $age . ' ' . I18N::translate('after death') . ')';
384                                // Family events which occur after death are probably errors
385                                if ($event->getParent() instanceof Family) {
386                                    $ageText .= '<i class="icon-warning"></i>';
387                                }
388                            }
389                        }
390                    }
391                    if ($ageText) {
392                        $html .= ' <span class="age">' . $ageText . '</span>';
393                    }
394                }
395            } elseif ($record instanceof Family) {
396                $indi = Individual::getInstance($pid, $record->getTree());
397                if ($indi) {
398                    $birth_date = $indi->getBirthDate();
399                    $death_date = $indi->getDeathDate();
400                    $ageText    = '';
401                    if (Date::compare($date, $death_date) <= 0) {
402                        $age = Date::getAgeGedcom($birth_date, $date);
403                        // Only show calculated age if it differs from recorded age
404                        if ($age != '' && $age > 0) {
405                            if (
406                                $fact_age != '' && $fact_age != $age ||
407                                $fact_age == '' && $husb_age == '' && $wife_age == '' ||
408                                $husb_age != '' && $indi->getSex() == 'M' && $husb_age != $age ||
409                                $wife_age != '' && $indi->getSex() == 'F' && $wife_age != $age
410                            ) {
411                                $ageText = '(' . I18N::translate('Age') . ' ' . FunctionsDate::getAgeAtEvent($age) . ')';
412                            }
413                        }
414                    }
415                    if ($ageText) {
416                        $html .= ' <span class="age">' . $ageText . '</span>';
417                    }
418                }
419            }
420        } elseif (strpos($factrec, "\n2 PLAC ") === false && in_array($fact, Config::emptyFacts())) {
421            // There is no DATE.  If there is also no PLAC, then print "yes"
422            $html .= I18N::translate('yes');
423        }
424        // print gedcom ages
425        foreach (array(GedcomTag::getLabel('AGE') => $fact_age, GedcomTag::getLabel('HUSB') => $husb_age, GedcomTag::getLabel('WIFE') => $wife_age) as $label => $age) {
426            if ($age != '') {
427                $html .= ' <span class="label">' . $label . ':</span> <span class="age">' . FunctionsDate::getAgeAtEvent($age) . '</span>';
428            }
429        }
430
431        return $html;
432    }
433
434    /**
435     * print fact PLACe TEMPle STATus
436     *
437     * @param Fact $event gedcom fact record
438     * @param bool $anchor to print a link to placelist
439     * @param bool $sub_records to print place subrecords
440     * @param bool $lds to print LDS TEMPle and STATus
441     *
442     * @return string HTML
443     */
444    public static function formatFactPlace(Fact $event, $anchor = false, $sub_records = false, $lds = false)
445    {
446        if ($anchor) {
447            // Show the full place name, for facts/events tab
448            $html = '<a href="' . $event->getPlace()->getURL() . '">' . $event->getPlace()->getFullName() . '</a>';
449        } else {
450            // Abbreviate the place name, for chart boxes
451            return $event->getPlace()->getShortName();
452        }
453
454        if ($sub_records) {
455            $placerec = Functions::getSubRecord(2, '2 PLAC', $event->getGedcom());
456            if (!empty($placerec)) {
457                if (preg_match_all('/\n3 (?:_HEB|ROMN) (.+)/', $placerec, $matches)) {
458                    foreach ($matches[1] as $match) {
459                        $wt_place = new Place($match, $event->getParent()->getTree());
460                        $html .= ' - ' . $wt_place->getFullName();
461                    }
462                }
463                $map_lati = "";
464                $cts      = preg_match('/\d LATI (.*)/', $placerec, $match);
465                if ($cts > 0) {
466                    $map_lati = $match[1];
467                    $html .= '<br><span class="label">' . GedcomTag::getLabel('LATI') . ': </span>' . $map_lati;
468                }
469                $map_long = '';
470                $cts      = preg_match('/\d LONG (.*)/', $placerec, $match);
471                if ($cts > 0) {
472                    $map_long = $match[1];
473                    $html .= ' <span class="label">' . GedcomTag::getLabel('LONG') . ': </span>' . $map_long;
474                }
475                if ($map_lati && $map_long) {
476                    $map_lati = trim(strtr($map_lati, "NSEW,�", " - -. ")); // S5,6789 ==> -5.6789
477                    $map_long = trim(strtr($map_long, "NSEW,�", " - -. ")); // E3.456� ==> 3.456
478                    $html .= ' <a rel="nofollow" href="https://maps.google.com/maps?q=' . $map_lati . ',' . $map_long . '" class="icon-googlemaps" title="' . I18N::translate('Google Maps') . '"></a>';
479                    $html .= ' <a rel="nofollow" href="https://www.bing.com/maps/?lvl=15&cp=' . $map_lati . '~' . $map_long . '" class="icon-bing" title="' . I18N::translate('Bing Maps™') . '"></a>';
480                    $html .= ' <a rel="nofollow" href="https://www.openstreetmap.org/#map=15/' . $map_lati . '/' . $map_long . '" class="icon-osm" title="' . I18N::translate('OpenStreetMap') . '"></a>';
481                }
482                if (preg_match('/\d NOTE (.*)/', $placerec, $match)) {
483                    $html .= '<br>' . self::printFactNotes($placerec, 3);
484                }
485            }
486        }
487        if ($lds) {
488            if (preg_match('/2 TEMP (.*)/', $event->getGedcom(), $match)) {
489                $html .= '<br>' . I18N::translate('LDS temple') . ': ' . GedcomCodeTemp::templeName($match[1]);
490            }
491            if (preg_match('/2 STAT (.*)/', $event->getGedcom(), $match)) {
492                $html .= '<br>' . I18N::translate('Status') . ': ' . GedcomCodeStat::statusName($match[1]);
493                if (preg_match('/3 DATE (.*)/', $event->getGedcom(), $match)) {
494                    $date = new Date($match[1]);
495                    $html .= ', ' . GedcomTag::getLabel('STAT:DATE') . ': ' . $date->display();
496                }
497            }
498        }
499
500        return $html;
501    }
502
503    /**
504     * Check for facts that may exist only once for a certain record type.
505     * If the fact already exists in the second array, delete it from the first one.
506     *
507     * @param string[] $uniquefacts
508     * @param Fact[] $recfacts
509     * @param string $type
510     *
511     * @return string[]
512     */
513    public static function checkFactUnique($uniquefacts, $recfacts, $type)
514    {
515        foreach ($recfacts as $factarray) {
516            $fact = false;
517            if (is_object($factarray)) {
518                $fact = $factarray->getTag();
519            } else {
520                if ($type === 'SOUR' || $type === 'REPO') {
521                    $factrec = $factarray[0];
522                }
523                if ($type === 'FAM' || $type === 'INDI') {
524                    $factrec = $factarray[1];
525                }
526
527                $ft = preg_match("/1 (\w+)(.*)/", $factrec, $match);
528                if ($ft > 0) {
529                    $fact = trim($match[1]);
530                }
531            }
532            if ($fact !== false) {
533                $key = array_search($fact, $uniquefacts);
534                if ($key !== false) {
535                    unset($uniquefacts[$key]);
536                }
537            }
538        }
539
540        return $uniquefacts;
541    }
542
543    /**
544     * Print a new fact box on details pages
545     *
546     * @param string $id the id of the person, family, source etc the fact will be added to
547     * @param array $usedfacts an array of facts already used in this record
548     * @param string $type the type of record INDI, FAM, SOUR etc
549     */
550    public static function printAddNewFact($id, $usedfacts, $type)
551    {
552        global $WT_TREE;
553
554        // -- Add from clipboard
555        if (is_array(Session::get('clipboard'))) {
556            $newRow = true;
557            foreach (array_reverse(Session::get('clipboard'), true) as $fact_id => $fact) {
558                if ($fact["type"] == $type || $fact["type"] == 'all') {
559                    if ($newRow) {
560                        $newRow = false;
561                        echo '<tr class="noprint"><td class="descriptionbox">';
562                        echo I18N::translate('Add from clipboard'), '</td>';
563                        echo '<td class="optionbox wrap"><form method="get" name="newFromClipboard" action="?" onsubmit="return false;">';
564                        echo '<select id="newClipboardFact">';
565                    }
566                    echo '<option value="', Filter::escapeHtml($fact_id), '">', GedcomTag::getLabel($fact['fact']);
567                    // TODO use the event class to store/parse the clipboard events
568                    if (preg_match('/^2 DATE (.+)/m', $fact['factrec'], $match)) {
569                        $tmp = new Date($match[1]);
570                        echo '; ', $tmp->minimumDate()->format('%Y');
571                    }
572                    if (preg_match('/^2 PLAC ([^,\n]+)/m', $fact['factrec'], $match)) {
573                        echo '; ', $match[1];
574                    }
575                    echo '</option>';
576                }
577            }
578            if (!$newRow) {
579                echo '</select>';
580                echo '&nbsp;&nbsp;<input type="button" value="', /* I18N: A button label. */ I18N::translate('add'), "\" onclick=\"return paste_fact('$id', '#newClipboardFact');\"> ";
581                echo '</form></td></tr>', "\n";
582            }
583        }
584
585        // -- Add from pick list
586        switch ($type) {
587            case "INDI":
588                $addfacts    = preg_split("/[, ;:]+/", $WT_TREE->getPreference('INDI_FACTS_ADD'), -1, PREG_SPLIT_NO_EMPTY);
589                $uniquefacts = preg_split("/[, ;:]+/", $WT_TREE->getPreference('INDI_FACTS_UNIQUE'), -1, PREG_SPLIT_NO_EMPTY);
590                $quickfacts  = preg_split("/[, ;:]+/", $WT_TREE->getPreference('INDI_FACTS_QUICK'), -1, PREG_SPLIT_NO_EMPTY);
591                break;
592            case "FAM":
593                $addfacts    = preg_split("/[, ;:]+/", $WT_TREE->getPreference('FAM_FACTS_ADD'), -1, PREG_SPLIT_NO_EMPTY);
594                $uniquefacts = preg_split("/[, ;:]+/", $WT_TREE->getPreference('FAM_FACTS_UNIQUE'), -1, PREG_SPLIT_NO_EMPTY);
595                $quickfacts  = preg_split("/[, ;:]+/", $WT_TREE->getPreference('FAM_FACTS_QUICK'), -1, PREG_SPLIT_NO_EMPTY);
596                break;
597            case "SOUR":
598                $addfacts    = preg_split("/[, ;:]+/", $WT_TREE->getPreference('SOUR_FACTS_ADD'), -1, PREG_SPLIT_NO_EMPTY);
599                $uniquefacts = preg_split("/[, ;:]+/", $WT_TREE->getPreference('SOUR_FACTS_UNIQUE'), -1, PREG_SPLIT_NO_EMPTY);
600                $quickfacts  = preg_split("/[, ;:]+/", $WT_TREE->getPreference('SOUR_FACTS_QUICK'), -1, PREG_SPLIT_NO_EMPTY);
601                break;
602            case "NOTE":
603                $addfacts    = preg_split("/[, ;:]+/", $WT_TREE->getPreference('NOTE_FACTS_ADD'), -1, PREG_SPLIT_NO_EMPTY);
604                $uniquefacts = preg_split("/[, ;:]+/", $WT_TREE->getPreference('NOTE_FACTS_UNIQUE'), -1, PREG_SPLIT_NO_EMPTY);
605                $quickfacts  = preg_split("/[, ;:]+/", $WT_TREE->getPreference('NOTE_FACTS_QUICK'), -1, PREG_SPLIT_NO_EMPTY);
606                break;
607            case "REPO":
608                $addfacts    = preg_split("/[, ;:]+/", $WT_TREE->getPreference('REPO_FACTS_ADD'), -1, PREG_SPLIT_NO_EMPTY);
609                $uniquefacts = preg_split("/[, ;:]+/", $WT_TREE->getPreference('REPO_FACTS_UNIQUE'), -1, PREG_SPLIT_NO_EMPTY);
610                $quickfacts  = preg_split("/[, ;:]+/", $WT_TREE->getPreference('REPO_FACTS_QUICK'), -1, PREG_SPLIT_NO_EMPTY);
611                break;
612            default:
613                return;
614        }
615        $addfacts            = array_merge(self::checkFactUnique($uniquefacts, $usedfacts, $type), $addfacts);
616        $quickfacts          = array_intersect($quickfacts, $addfacts);
617        $translated_addfacts = array();
618        foreach ($addfacts as $addfact) {
619            $translated_addfacts[$addfact] = GedcomTag::getLabel($addfact);
620        }
621        uasort($translated_addfacts, function ($x, $y) {
622            return I18N::strcasecmp(I18N::translate($x), I18N::translate($y));
623        });
624        echo '<tr class="noprint"><td class="descriptionbox">';
625        echo I18N::translate('Fact or event');
626        echo '</td>';
627        echo '<td class="optionbox wrap">';
628        echo '<form method="get" name="newfactform" action="?" onsubmit="return false;">';
629        echo '<select id="newfact" name="newfact">';
630        echo '<option value="" disabled selected>' . I18N::translate('&lt;select&gt;') . '</option>';
631        foreach ($translated_addfacts as $fact => $fact_name) {
632            echo '<option value="', $fact, '">', $fact_name, '</option>';
633        }
634        if ($type == 'INDI' || $type == 'FAM') {
635            echo '<option value="FACT">', I18N::translate('Custom fact'), '</option>';
636            echo '<option value="EVEN">', I18N::translate('Custom event'), '</option>';
637        }
638        echo '</select>';
639        echo '<input type="button" value="', /* I18N: A button label. */ I18N::translate('add'), '" onclick="add_record(\'' . $id . '\', \'newfact\');">';
640        echo '<span class="quickfacts">';
641        foreach ($quickfacts as $fact) {
642            echo '<a href="#" onclick="add_new_record(\'' . $id . '\', \'' . $fact . '\');return false;">', GedcomTag::getLabel($fact), '</a>';
643        }
644        echo '</span></form>';
645        echo '</td></tr>';
646    }
647
648    /**
649     * javascript declaration for calendar popup
650     */
651    public static function initializeCalendarPopup()
652    {
653        global $controller;
654
655        $controller->addInlineJavascript('
656			cal_setMonthNames(
657				"' . I18N::translateContext('NOMINATIVE', 'January') . '",
658				"' . I18N::translateContext('NOMINATIVE', 'February') . '",
659				"' . I18N::translateContext('NOMINATIVE', 'March') . '",
660				"' . I18N::translateContext('NOMINATIVE', 'April') . '",
661				"' . I18N::translateContext('NOMINATIVE', 'May') . '",
662				"' . I18N::translateContext('NOMINATIVE', 'June') . '",
663				"' . I18N::translateContext('NOMINATIVE', 'July') . '",
664				"' . I18N::translateContext('NOMINATIVE', 'August') . '",
665				"' . I18N::translateContext('NOMINATIVE', 'September') . '",
666				"' . I18N::translateContext('NOMINATIVE', 'October') . '",
667				"' . I18N::translateContext('NOMINATIVE', 'November') . '",
668				"' . I18N::translateContext('NOMINATIVE', 'December') . '"
669			)
670			cal_setDayHeaders(
671				"' . I18N::translate('Sun') . '",
672				"' . I18N::translate('Mon') . '",
673				"' . I18N::translate('Tue') . '",
674				"' . I18N::translate('Wed') . '",
675				"' . I18N::translate('Thu') . '",
676				"' . I18N::translate('Fri') . '",
677				"' . I18N::translate('Sat') . '"
678			)
679			cal_setWeekStart(' . I18N::firstDay() . ');
680		');
681    }
682
683    /**
684     * HTML link to find an individual.
685     *
686     * @param string $element_id
687     * @param string $indiname
688     * @param Tree $tree
689     *
690     * @return string
691     */
692    public static function printFindIndividualLink($element_id, $indiname = '', $tree = null)
693    {
694        global $WT_TREE;
695
696        if ($tree === null) {
697            $tree = $WT_TREE;
698        }
699
700        return '<a href="#" onclick="findIndi(document.getElementById(\'' . $element_id . '\'), document.getElementById(\'' . $indiname . '\'), \'' . $tree->getNameHtml() . '\'); return false;" class="icon-button_indi" title="' . I18N::translate('Find an individual') . '"></a>';
701    }
702
703    /**
704     * HTML link to find a place.
705     *
706     * @param string $element_id
707     *
708     * @return string
709     */
710    public static function printFindPlaceLink($element_id)
711    {
712        return '<a href="#" onclick="findPlace(document.getElementById(\'' . $element_id . '\'), WT_GEDCOM); return false;" class="icon-button_place" title="' . I18N::translate('Find a place') . '"></a>';
713    }
714
715    /**
716     * HTML link to find a family.
717     *
718     * @param string $element_id
719     *
720     * @return string
721     */
722    public static function printFindFamilyLink($element_id)
723    {
724        return '<a href="#" onclick="findFamily(document.getElementById(\'' . $element_id . '\'), WT_GEDCOM); return false;" class="icon-button_family" title="' . I18N::translate('Find a family') . '"></a>';
725    }
726
727    /**
728     * HTML link to open the special character window.
729     *
730     * @param string $element_id
731     *
732     * @return string
733     */
734    public static function printSpecialCharacterLink($element_id)
735    {
736        return '<span onclick="findSpecialChar(document.getElementById(\'' . $element_id . '\')); if (window.updatewholename) { updatewholename(); } return false;" class="icon-button_keyboard" title="' . I18N::translate('Find a special character') . '"></span>';
737    }
738
739    /**
740     * HTML element to insert a value from a list.
741     *
742     * @param string $element_id
743     * @param string[] $choices
744     */
745    public static function printAutoPasteLink($element_id, $choices)
746    {
747        echo '<small>';
748        foreach ($choices as $choice) {
749            echo '<span onclick="document.getElementById(\'', $element_id, '\').value=';
750            echo '\'', $choice, '\';';
751            echo " return false;\">", $choice, '</span> ';
752        }
753        echo '</small>';
754    }
755
756    /**
757     * HTML link to find a source.
758     *
759     * @param string $element_id
760     * @param string $sourcename
761     *
762     * @return string
763     */
764    public static function printFindSourceLink($element_id, $sourcename = '')
765    {
766        return '<a href="#" onclick="findSource(document.getElementById(\'' . $element_id . '\'), document.getElementById(\'' . $sourcename . '\'), WT_GEDCOM); return false;" class="icon-button_source" title="' . I18N::translate('Find a source') . '"></a>';
767    }
768
769    /**
770     * HTML link to find a note.
771     *
772     * @param string $element_id
773     * @param string $notename
774     *
775     * @return string
776     */
777    public static function printFindNoteLink($element_id, $notename = '')
778    {
779        return '<a href="#" onclick="findnote(document.getElementById(\'' . $element_id . '\'), document.getElementById(\'' . $notename . '\'), \'WT_GEDCOM\'); return false;" class="icon-button_find" title="' . I18N::translate('Find a shared note') . '"></a>';
780    }
781
782    /**
783     * HTML link to find a repository.
784     *
785     * @param string $element_id
786     *
787     * @return string
788     */
789    public static function printFindRepositoryLink($element_id)
790    {
791        return '<a href="#" onclick="findRepository(document.getElementById(\'' . $element_id . '\'), WT_GEDCOM); return false;" class="icon-button_repository" title="' . I18N::translate('Find a repository') . '"></a>';
792    }
793
794    /**
795     * HTML link to find a media object.
796     *
797     * @param string $element_id
798     * @param string $choose
799     *
800     * @return string
801     */
802    public static function printFindMediaLink($element_id, $choose = '')
803    {
804        return '<a href="#" onclick="findMedia(document.getElementById(\'' . $element_id . '\'), \'' . $choose . '\', WT_GEDCOM); return false;" class="icon-button_media" title="' . I18N::translate('Find a media object') . '"></a>';
805    }
806
807    /**
808     * HTML link to find a fact.
809     *
810     * @param string $element_id
811     *
812     * @return string
813     */
814    public static function printFindFactLink($element_id)
815    {
816        return '<a href="#" onclick="findFact(document.getElementById(\'' . $element_id . '\'), WT_GEDCOM); return false;" class="icon-button_find_facts" title="' . I18N::translate('Find a fact or event') . '"></a>';
817    }
818
819    /**
820     * Summary of LDS ordinances.
821     *
822     * @param Individual $individual
823     *
824     * @return string
825     */
826    public static function getLdsSummary(Individual $individual)
827    {
828        $BAPL = $individual->getFacts('BAPL') ? 'B' : '_';
829        $ENDL = $individual->getFacts('ENDL') ? 'E' : '_';
830        $SLGC = $individual->getFacts('SLGC') ? 'C' : '_';
831        $SLGS = '_';
832
833        foreach ($individual->getSpouseFamilies() as $family) {
834            if ($family->getFacts('SLGS')) {
835                $SLGS = '';
836            }
837        }
838
839        return $BAPL . $ENDL . $SLGS . $SLGC;
840    }
841}
842