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\Auth;
19use Fisharebest\Webtrees\Census\Census;
20use Fisharebest\Webtrees\Census\CensusOfCzechRepublic;
21use Fisharebest\Webtrees\Census\CensusOfDenmark;
22use Fisharebest\Webtrees\Census\CensusOfDeutschland;
23use Fisharebest\Webtrees\Census\CensusOfEngland;
24use Fisharebest\Webtrees\Census\CensusOfFrance;
25use Fisharebest\Webtrees\Census\CensusOfScotland;
26use Fisharebest\Webtrees\Census\CensusOfUnitedStates;
27use Fisharebest\Webtrees\Census\CensusOfWales;
28use Fisharebest\Webtrees\Config;
29use Fisharebest\Webtrees\Date;
30use Fisharebest\Webtrees\Fact;
31use Fisharebest\Webtrees\Family;
32use Fisharebest\Webtrees\Filter;
33use Fisharebest\Webtrees\GedcomCode\GedcomCodeAdop;
34use Fisharebest\Webtrees\GedcomCode\GedcomCodeName;
35use Fisharebest\Webtrees\GedcomCode\GedcomCodePedi;
36use Fisharebest\Webtrees\GedcomCode\GedcomCodeQuay;
37use Fisharebest\Webtrees\GedcomCode\GedcomCodeRela;
38use Fisharebest\Webtrees\GedcomCode\GedcomCodeStat;
39use Fisharebest\Webtrees\GedcomCode\GedcomCodeTemp;
40use Fisharebest\Webtrees\GedcomRecord;
41use Fisharebest\Webtrees\GedcomTag;
42use Fisharebest\Webtrees\I18N;
43use Fisharebest\Webtrees\Individual;
44use Fisharebest\Webtrees\Media;
45use Fisharebest\Webtrees\Module;
46use Fisharebest\Webtrees\Note;
47use Fisharebest\Webtrees\Repository;
48use Fisharebest\Webtrees\Source;
49use Fisharebest\Webtrees\User;
50use Rhumsaa\Uuid\Uuid;
51
52/**
53 * Class FunctionsEdit - common functions
54 */
55class FunctionsEdit
56{
57    /**
58     * Create a <select> control for a form.
59     *
60     * @param string $name
61     * @param string[] $values
62     * @param string|null $empty
63     * @param string $selected
64     * @param string $extra
65     *
66     * @return string
67     */
68    public static function selectEditControl($name, $values, $empty, $selected, $extra = '')
69    {
70        if ($empty === null) {
71            $html = '';
72        } else {
73            if (empty($selected)) {
74                $html = '<option value="" selected>' . Filter::escapeHtml($empty) . '</option>';
75            } else {
76                $html = '<option value="">' . Filter::escapeHtml($empty) . '</option>';
77            }
78        }
79        // A completely empty list would be invalid, and break various things
80        if (empty($values) && empty($html)) {
81            $html = '<option value=""></option>';
82        }
83        foreach ($values as $key => $value) {
84            // PHP array keys are cast to integers!  Cast them back
85            if ((string) $key === (string) $selected) {
86                $html .= '<option value="' . Filter::escapeHtml($key) . '" selected dir="auto">' . Filter::escapeHtml($value) . '</option>';
87            } else {
88                $html .= '<option value="' . Filter::escapeHtml($key) . '" dir="auto">' . Filter::escapeHtml($value) . '</option>';
89            }
90        }
91        if (substr($name, -2) === '[]') {
92            // id attribute is not used for arrays
93            return '<select name="' . $name . '" ' . $extra . '>' . $html . '</select>';
94        } else {
95            return '<select id="' . $name . '" name="' . $name . '" ' . $extra . '>' . $html . '</select>';
96        }
97    }
98
99    /**
100     * Create a set of radio buttons for a form
101     *
102     * @param string $name The ID for the form element
103     * @param string[] $values Array of value=>display items
104     * @param string $selected The currently selected item
105     * @param string $extra Additional markup for the label
106     *
107     * @return string
108     */
109    public static function radioButtons($name, $values, $selected, $extra = '')
110    {
111        $html = '';
112        foreach ($values as $key => $value) {
113            $html .=
114                '<label ' . $extra . '>' .
115                '<input type="radio" name="' . $name . '" value="' . Filter::escapeHtml($key) . '"';
116            // PHP array keys are cast to integers!  Cast them back
117            if ((string) $key === (string) $selected) {
118                $html .= ' checked';
119            }
120            $html .= '>' . Filter::escapeHtml($value) . '</label>';
121        }
122
123        return $html;
124    }
125
126    /**
127     * Print an edit control for a Yes/No field
128     *
129     * @param string $name
130     * @param bool $selected
131     * @param string $extra
132     *
133     * @return string
134     */
135    public static function editFieldYesNo($name, $selected = false, $extra = '')
136    {
137        return self::radioButtons(
138            $name, array(I18N::translate('no'), I18N::translate('yes')), $selected, $extra
139        );
140    }
141
142    /**
143     * Print an edit control for a checkbox.
144     *
145     * @param string $name
146     * @param bool $is_checked
147     * @param string $extra
148     *
149     * @return string
150     */
151    public static function checkbox($name, $is_checked = false, $extra = '')
152    {
153        return '<input type="checkbox" name="' . $name . '" value="1" ' . ($is_checked ? 'checked ' : '') . $extra . '>';
154    }
155
156    /**
157     * Print an edit control for a checkbox, with a hidden field to store one of the two states.
158     * By default, a checkbox is either set, or not sent.
159     * This function gives us a three options, set, unset or not sent.
160     * Useful for dynamically generated forms where we don't know what elements are present.
161     *
162     * @param string $name
163     * @param int $is_checked 0 or 1
164     * @param string $extra
165     *
166     * @return string
167     */
168    public static function twoStateCheckbox($name, $is_checked = 0, $extra = '')
169    {
170        return
171            '<input type="hidden" id="' . $name . '" name="' . $name . '" value="' . ($is_checked ? 1 : 0) . '">' .
172            '<input type="checkbox" name="' . $name . '-GUI-ONLY" value="1"' .
173            ($is_checked ? ' checked' : '') .
174            ' onclick="document.getElementById(\'' . $name . '\').value=(this.checked?1:0);" ' . $extra . '>';
175    }
176
177    /**
178     * Function edit_language_checkboxes
179     *
180     * @param string $parameter_name
181     * @param array $accepted_languages
182     *
183     * @return string
184     */
185    public static function editLanguageCheckboxes($parameter_name, $accepted_languages)
186    {
187        $html = '';
188        foreach (I18N::activeLocales() as $locale) {
189            $html .= '<div class="checkbox">';
190            $html .= '<label title="' . $locale->languageTag() . '">';
191            $html .= '<input type="checkbox" name="' . $parameter_name . '[]" value="' . $locale->languageTag() . '"';
192            $html .= in_array($locale->languageTag(), $accepted_languages) ? ' checked>' : '>';
193            $html .= $locale->endonym();
194            $html .= '</label>';
195            $html .= '</div>';
196        }
197
198        return $html;
199    }
200
201    /**
202     * Print an edit control for access level.
203     *
204     * @param string $name
205     * @param string $selected
206     * @param string $extra
207     *
208     * @return string
209     */
210    public static function editFieldAccessLevel($name, $selected = '', $extra = '')
211    {
212        $ACCESS_LEVEL = array(
213            Auth::PRIV_PRIVATE => I18N::translate('Show to visitors'),
214            Auth::PRIV_USER    => I18N::translate('Show to members'),
215            Auth::PRIV_NONE    => I18N::translate('Show to managers'),
216            Auth::PRIV_HIDE    => I18N::translate('Hide from everyone'),
217        );
218
219        return self::selectEditControl($name, $ACCESS_LEVEL, null, $selected, $extra);
220    }
221
222    /**
223     * Print an edit control for a RESN field.
224     *
225     * @param string $name
226     * @param string $selected
227     * @param string $extra
228     *
229     * @return string
230     */
231    public static function editFieldRestriction($name, $selected = '', $extra = '')
232    {
233        $RESN = array(
234            ''             => '',
235            'none'         => I18N::translate('Show to visitors'), // Not valid GEDCOM, but very useful
236            'privacy'      => I18N::translate('Show to members'),
237            'confidential' => I18N::translate('Show to managers'),
238            'locked'       => I18N::translate('Only managers can edit'),
239        );
240
241        return self::selectEditControl($name, $RESN, null, $selected, $extra);
242    }
243
244    /**
245     * Print an edit control for a contact method field.
246     *
247     * @param string $name
248     * @param string $selected
249     * @param string $extra
250     *
251     * @return string
252     */
253    public static function editFieldContact($name, $selected = '', $extra = '')
254    {
255        // Different ways to contact the users
256        $CONTACT_METHODS = array(
257            'messaging'  => I18N::translate('Internal messaging'),
258            'messaging2' => I18N::translate('Internal messaging with emails'),
259            'messaging3' => I18N::translate('webtrees sends emails with no storage'),
260            'mailto'     => I18N::translate('Mailto link'),
261            'none'       => I18N::translate('No contact'),
262        );
263
264        return self::selectEditControl($name, $CONTACT_METHODS, null, $selected, $extra);
265    }
266
267    /**
268     * Print an edit control for a language field.
269     *
270     * @param string $name
271     * @param string $selected
272     * @param string $extra
273     *
274     * @return string
275     */
276    public static function editFieldLanguage($name, $selected = '', $extra = '')
277    {
278        $languages = array();
279        foreach (I18N::activeLocales() as $locale) {
280            $languages[$locale->languageTag()] = $locale->endonym();
281        }
282
283        return self::selectEditControl($name, $languages, null, $selected, $extra);
284    }
285
286    /**
287     * Print an edit control for a range of integers.
288     *
289     * @param string $name
290     * @param string $selected
291     * @param int $min
292     * @param int $max
293     * @param string $extra
294     *
295     * @return string
296     */
297    public static function editFieldInteger($name, $selected = '', $min, $max, $extra = '')
298    {
299        $array = array();
300        for ($i = $min; $i <= $max; ++$i) {
301            $array[$i] = I18N::number($i);
302        }
303
304        return self::selectEditControl($name, $array, null, $selected, $extra);
305    }
306
307    /**
308     * Print an edit control for a username.
309     *
310     * @param string $name
311     * @param string $selected
312     * @param string $extra
313     *
314     * @return string
315     */
316    public static function editFieldUsername($name, $selected = '', $extra = '')
317    {
318        $users = array();
319        foreach (User::all() as $user) {
320            $users[$user->getUserName()] = $user->getRealName() . ' - ' . $user->getUserName();
321        }
322        // The currently selected user may not exist
323        if ($selected && !array_key_exists($selected, $users)) {
324            $users[$selected] = $selected;
325        }
326
327        return self::selectEditControl($name, $users, '-', $selected, $extra);
328    }
329
330    /**
331     * Print an edit control for a ADOP field.
332     *
333     * @param string          $name
334     * @param string          $selected
335     * @param string          $extra
336     * @param Individual|null $individual
337     *
338     * @return string
339     */
340    public static function editFieldAdoption($name, $selected = '', $extra = '', Individual $individual = null)
341    {
342        return self::selectEditControl($name, GedcomCodeAdop::getValues($individual), null, $selected, $extra);
343    }
344
345    /**
346     * Print an edit control for a PEDI field.
347     *
348     * @param string          $name
349     * @param string          $selected
350     * @param string          $extra
351     * @param Individual|null $individual
352     *
353     * @return string
354     */
355    public static function editFieldPedigree($name, $selected = '', $extra = '', Individual $individual = null)
356    {
357        return self::selectEditControl($name, GedcomCodePedi::getValues($individual), '', $selected, $extra);
358    }
359
360    /**
361     * Print an edit control for a NAME TYPE field.
362     *
363     * @param string          $name
364     * @param string          $selected
365     * @param string          $extra
366     * @param Individual|null $individual
367     *
368     * @return string
369     */
370    public static function editFieldNameType($name, $selected = '', $extra = '', Individual $individual = null)
371    {
372        return self::selectEditControl($name, GedcomCodeName::getValues($individual), '', $selected, $extra);
373    }
374
375    /**
376     * Print an edit control for a RELA field.
377     *
378     * @param string $name
379     * @param string $selected
380     * @param string $extra
381     *
382     * @return string
383     */
384    public static function editFieldRelationship($name, $selected = '', $extra = '')
385    {
386        $rela_codes = GedcomCodeRela::getValues();
387        // The user is allowed to specify values that aren't in the list.
388        if (!array_key_exists($selected, $rela_codes)) {
389            $rela_codes[$selected] = I18N::translate($selected);
390        }
391
392        return self::selectEditControl($name, $rela_codes, '', $selected, $extra);
393    }
394
395    /**
396     * Remove all links from $gedrec to $xref, and any sub-tags.
397     *
398     * @param string $gedrec
399     * @param string $xref
400     *
401     * @return string
402     */
403    public static function removeLinks($gedrec, $xref)
404    {
405        $gedrec = preg_replace('/\n1 ' . WT_REGEX_TAG . ' @' . $xref . '@(\n[2-9].*)*/', '', $gedrec);
406        $gedrec = preg_replace('/\n2 ' . WT_REGEX_TAG . ' @' . $xref . '@(\n[3-9].*)*/', '', $gedrec);
407        $gedrec = preg_replace('/\n3 ' . WT_REGEX_TAG . ' @' . $xref . '@(\n[4-9].*)*/', '', $gedrec);
408        $gedrec = preg_replace('/\n4 ' . WT_REGEX_TAG . ' @' . $xref . '@(\n[5-9].*)*/', '', $gedrec);
409        $gedrec = preg_replace('/\n5 ' . WT_REGEX_TAG . ' @' . $xref . '@(\n[6-9].*)*/', '', $gedrec);
410
411        return $gedrec;
412    }
413
414    /**
415     * Generates javascript code for calendar popup in user’s language.
416     *
417     * @param string $id
418     *
419     * @return string
420     */
421    public static function printCalendarPopup($id)
422    {
423        return
424            ' <a href="#" onclick="cal_toggleDate(\'caldiv' . $id . '\', \'' . $id . '\'); return false;" class="icon-button_calendar" title="' . I18N::translate('Select a date') . '"></a>' .
425            '<div id="caldiv' . $id . '" style="position:absolute;visibility:hidden;background-color:white;z-index:1000;"></div>';
426    }
427
428    /**
429     * An HTML link to create a new media object.
430     *
431     * @param string $element_id
432     *
433     * @return string
434     */
435    public static function printAddNewMediaLink($element_id)
436    {
437        return '<a href="#" onclick="pastefield=document.getElementById(\'' . $element_id . '\'); window.open(\'addmedia.php?action=showmediaform\', \'_blank\', edit_window_specs); return false;" class="icon-button_addmedia" title="' . I18N::translate('Create a media object') . '"></a>';
438    }
439
440    /**
441     * An HTML link to create a new repository.
442     *
443     * @param string $element_id
444     *
445     * @return string
446     */
447    public static function printAddNewRepositoryLink($element_id)
448    {
449        return '<a href="#" onclick="addnewrepository(document.getElementById(\'' . $element_id . '\')); return false;" class="icon-button_addrepository" title="' . I18N::translate('Create a repository') . '"></a>';
450    }
451
452    /**
453     * An HTML link to create a new note.
454     *
455     * @param string $element_id
456     *
457     * @return string
458     */
459    public static function printAddNewNoteLink($element_id)
460    {
461        return '<a href="#" onclick="addnewnote(document.getElementById(\'' . $element_id . '\')); return false;" class="icon-button_addnote" title="' . I18N::translate('Create a shared note') . '"></a>';
462    }
463
464    /**
465     * An HTML link to edit a note.
466     *
467     * @param string $note_id
468     *
469     * @return string
470     */
471    public static function printEditNoteLink($note_id)
472    {
473        return '<a href="#" onclick="edit_note(\'' . $note_id . '\'); return false;" class="icon-button_note" title="' . I18N::translate('Edit the shared note') . '"></a>';
474    }
475
476    /**
477     * An HTML link to create a new source.
478     *
479     * @param string $element_id
480     *
481     * @return string
482     */
483    public static function printAddNewSourceLink($element_id)
484    {
485        return '<a href="#" onclick="addnewsource(document.getElementById(\'' . $element_id . '\')); return false;" class="icon-button_addsource" title="' . I18N::translate('Create a source') . '"></a>';
486    }
487
488    /**
489     * add a new tag input field
490     *
491     * called for each fact to be edited on a form.
492     * Fact level=0 means a new empty form : data are POSTed by name
493     * else data are POSTed using arrays :
494     * glevels[] : tag level
495     *  islink[] : tag is a link
496     *     tag[] : tag name
497     *    text[] : tag value
498     *
499     * @param string $tag fact record to edit (eg 2 DATE xxxxx)
500     * @param string $upperlevel optional upper level tag (eg BIRT)
501     * @param string $label An optional label to echo instead of the default
502     * @param string $extra optional text to display after the input field
503     * @param Individual $person For male/female translations
504     *
505     * @return string
506     */
507    public static function addSimpleTag($tag, $upperlevel = '', $label = '', $extra = null, Individual $person = null)
508    {
509        global $tags, $main_fact, $xref, $bdm, $action, $WT_TREE;
510
511        // Keep track of SOUR fields, so we can reference them in subsequent PAGE fields.
512        static $source_element_id;
513
514        $subnamefacts = array('NPFX', 'GIVN', 'SPFX', 'SURN', 'NSFX', '_MARNM_SURN');
515        preg_match('/^(?:(\d+) (' . WT_REGEX_TAG . ') ?(.*))/', $tag, $match);
516        list(, $level, $fact, $value) = $match;
517        $level                        = (int) $level;
518
519        // element name : used to POST data
520        if ($level === 0) {
521            if ($upperlevel) {
522                $element_name = $upperlevel . '_' . $fact;
523            } else {
524                $element_name = $fact;
525            }
526        } else {
527            $element_name = 'text[]';
528        }
529        if ($level === 1) {
530            $main_fact = $fact;
531        }
532
533        // element id : used by javascript functions
534        if ($level === 0) {
535            $element_id = $fact;
536        } else {
537            $element_id = $fact . Uuid::uuid4();
538        }
539        if ($upperlevel) {
540            $element_id = $upperlevel . '_' . $fact . Uuid::uuid4();
541        }
542
543        // field value
544        $islink = (substr($value, 0, 1) === '@' && substr($value, 0, 2) !== '@#');
545        if ($islink) {
546            $value = trim(substr($tag, strlen($fact) + 3), '@');
547        } else {
548            $value = (string) substr($tag, strlen($fact) + 3);
549        }
550        if ($fact === 'REPO' || $fact === 'SOUR' || $fact === 'OBJE' || $fact === 'FAMC') {
551            $islink = true;
552        }
553
554        if ($fact === 'SHARED_NOTE_EDIT' || $fact === 'SHARED_NOTE') {
555            $islink = true;
556            $fact   = 'NOTE';
557        }
558
559        // label
560        echo '<tr id="', $element_id, '_tr"';
561        if ($fact === 'DATA' || $fact === 'MAP' || ($fact === 'LATI' || $fact === 'LONG') && $value === '') {
562            echo ' style="display:none;"';
563        }
564        echo '>';
565
566        if (in_array($fact, $subnamefacts) || $fact === 'LATI' || $fact === 'LONG') {
567            echo '<td class="optionbox wrap width25">';
568        } else {
569            echo '<td class="descriptionbox wrap width25">';
570        }
571
572        // tag name
573        if ($label) {
574            echo $label;
575        } elseif ($upperlevel) {
576            echo GedcomTag::getLabel($upperlevel . ':' . $fact);
577        } else {
578            echo GedcomTag::getLabel($fact);
579        }
580
581        // If using GEDFact-assistant window
582        if ($action === 'addnewnote_assisted') {
583            // Do not print on GEDFact Assistant window
584        } else {
585            // Not all facts have help text.
586            switch ($fact) {
587                case 'NAME':
588                    if ($upperlevel !== 'REPO' && $upperlevel !== 'UNKNOWN') {
589                        echo FunctionsPrint::helpLink($fact);
590                    }
591                    break;
592                case 'DATE':
593                case 'PLAC':
594                case 'RESN':
595                case 'ROMN':
596                case 'SURN':
597                case '_HEB':
598                    echo FunctionsPrint::helpLink($fact);
599                    break;
600            }
601        }
602        // tag level
603        if ($level > 0) {
604            echo '<input type="hidden" name="glevels[]" value="', $level, '">';
605            echo '<input type="hidden" name="islink[]" value="', $islink, '">';
606            echo '<input type="hidden" name="tag[]" value="', $fact, '">';
607        }
608        echo '</td>';
609
610        // value
611        echo '<td class="optionbox wrap">';
612
613        // retrieve linked NOTE
614        if ($fact === 'NOTE' && $islink) {
615            $note1 = Note::getInstance($value, $WT_TREE);
616            if ($note1) {
617                $noterec = $note1->getGedcom();
618                preg_match('/' . $value . '/i', $noterec, $notematch);
619                $value = $notematch[0];
620            }
621        }
622
623        // Show names for spouses in MARR/HUSB/AGE and MARR/WIFE/AGE
624        if ($fact === 'HUSB' || $fact === 'WIFE') {
625            $family = Family::getInstance($xref, $WT_TREE);
626            if ($family) {
627                $spouse_link = $family->getFirstFact($fact);
628                if ($spouse_link) {
629                    $spouse = $spouse_link->getTarget();
630                    if ($spouse) {
631                        echo $spouse->getFullName();
632                    }
633                }
634            }
635        }
636
637        if (in_array($fact, Config::emptyFacts()) && ($value === '' || $value === 'Y' || $value === 'y')) {
638            echo '<input type="hidden" id="', $element_id, '" name="', $element_name, '" value="', $value, '">';
639            if ($level <= 1) {
640                echo '<input type="checkbox" ';
641                if ($value) {
642                    echo 'checked';
643                }
644                echo ' onclick="document.getElementById(\'' . $element_id . '\').value = (this.checked) ? \'Y\' : \'\';">';
645                echo I18N::translate('yes');
646            }
647
648            if ($fact === 'CENS' && $value === 'Y') {
649                echo self::censusDateSelector(WT_LOCALE, $xref);
650                if (Module::getModuleByName('GEDFact_assistant') && GedcomRecord::getInstance($xref, $WT_TREE) instanceof Individual) {
651                    echo
652                        '<div></div><a href="#" style="display: none;" id="assistant-link" onclick="return activateCensusAssistant();">' .
653                        I18N::translate('Create a shared note using the census assistant') .
654                        '</a></div>';
655                }
656            }
657        } elseif ($fact === 'TEMP') {
658            echo self::selectEditControl($element_name, GedcomCodeTemp::templeNames(), I18N::translate('No temple - living ordinance'), $value);
659        } elseif ($fact === 'ADOP') {
660            echo self::editFieldAdoption($element_name, $value, '', $person);
661        } elseif ($fact === 'PEDI') {
662            echo self::editFieldPedigree($element_name, $value, '', $person);
663        } elseif ($fact === 'STAT') {
664            echo self::selectEditControl($element_name, GedcomCodeStat::statusNames($upperlevel), '', $value);
665        } elseif ($fact === 'RELA') {
666            echo self::editFieldRelationship($element_name, strtolower($value));
667        } elseif ($fact === 'QUAY') {
668            echo self::selectEditControl($element_name, GedcomCodeQuay::getValues(), '', $value);
669        } elseif ($fact === '_WT_USER') {
670            echo self::editFieldUsername($element_name, $value);
671        } elseif ($fact === 'RESN') {
672            echo self::editFieldRestriction($element_name, $value);
673        } elseif ($fact === '_PRIM') {
674            echo '<select id="', $element_id, '" name="', $element_name, '" >';
675            echo '<option value=""></option>';
676            echo '<option value="Y" ';
677            if ($value === 'Y') {
678                echo ' selected';
679            }
680            echo '>', /* I18N: option in list box “always use this image” */
681            I18N::translate('always'), '</option>';
682            echo '<option value="N" ';
683            if ($value === 'N') {
684                echo 'selected';
685            }
686            echo '>', /* I18N: option in list box “never use this image” */
687            I18N::translate('never'), '</option>';
688            echo '</select>';
689            echo '<p class="small text-muted">', I18N::translate('Use this image for charts and on the individual’s page.'), '</p>';
690        } elseif ($fact === 'SEX') {
691            echo '<select id="', $element_id, '" name="', $element_name, '"><option value="M" ';
692            if ($value === 'M') {
693                echo 'selected';
694            }
695            echo '>', I18N::translate('Male'), '</option><option value="F" ';
696            if ($value === 'F') {
697                echo 'selected';
698            }
699            echo '>', I18N::translate('Female'), '</option><option value="U" ';
700            if ($value === 'U' || empty($value)) {
701                echo 'selected';
702            }
703            echo '>', I18N::translateContext('unknown gender', 'Unknown'), '</option></select>';
704        } elseif ($fact === 'TYPE' && $level === 3) {
705            //-- Build the selector for the Media 'TYPE' Fact
706            echo '<select name="text[]"><option selected value="" ></option>';
707            $selectedValue = strtolower($value);
708            if (!array_key_exists($selectedValue, GedcomTag::getFileFormTypes())) {
709                echo '<option selected value="', Filter::escapeHtml($value), '" >', Filter::escapeHtml($value), '</option>';
710            }
711            foreach (GedcomTag::getFileFormTypes() as $typeName => $typeValue) {
712                echo '<option value="', $typeName, '" ';
713                if ($selectedValue === $typeName) {
714                    echo 'selected';
715                }
716                echo '>', $typeValue, '</option>';
717            }
718            echo '</select>';
719        } elseif (($fact === 'NAME' && $upperlevel !== 'REPO' && $upperlevel !== 'UNKNOWN') || $fact === '_MARNM') {
720            // Populated in javascript from sub-tags
721            echo '<input type="hidden" id="', $element_id, '" name="', $element_name, '" onchange="updateTextName(\'', $element_id, '\');" value="', Filter::escapeHtml($value), '" class="', $fact, '">';
722            echo '<span id="', $element_id, '_display" dir="auto">', Filter::escapeHtml($value), '</span>';
723            echo ' <a href="#edit_name" onclick="convertHidden(\'', $element_id, '\'); return false;" class="icon-edit_indi" title="' . I18N::translate('Edit the name') . '"></a>';
724        } else {
725            // textarea
726            if ($fact === 'TEXT' || $fact === 'ADDR' || ($fact === 'NOTE' && !$islink)) {
727                echo '<textarea id="', $element_id, '" name="', $element_name, '" dir="auto">', Filter::escapeHtml($value), '</textarea><br>';
728            } else {
729                // text
730                // If using GEDFact-assistant window
731                if ($action === 'addnewnote_assisted') {
732                    echo '<input type="text" id="', $element_id, '" name="', $element_name, '" value="', Filter::escapeHtml($value), '" style="width:4.1em;" dir="ltr"';
733                } else {
734                    echo '<input type="text" id="', $element_id, '" name="', $element_name, '" value="', Filter::escapeHtml($value), '" dir="ltr"';
735                }
736                echo ' class="', $fact, '"';
737                if (in_array($fact, $subnamefacts)) {
738                    echo ' onblur="updatewholename();" onkeyup="updatewholename();"';
739                }
740
741                // Extra markup for specific fact types
742                switch ($fact) {
743                    case 'ALIA':
744                    case 'ASSO':
745                    case '_ASSO':
746                        echo ' data-autocomplete-type="ASSO" data-autocomplete-extra="input.DATE"';
747                        break;
748                    case 'DATE':
749                        echo ' onblur="valid_date(this);" onmouseout="valid_date(this);"';
750                        break;
751                    case 'GIVN':
752                        echo ' autofocus data-autocomplete-type="GIVN"';
753                        break;
754                    case 'LATI':
755                        echo ' onblur="valid_lati_long(this, \'N\', \'S\');" onmouseout="valid_lati_long(this, \'N\', \'S\');"';
756                        break;
757                    case 'LONG':
758                        echo ' onblur="valid_lati_long(this, \'E\', \'W\');" onmouseout="valid_lati_long(this, \'E\', \'W\');"';
759                        break;
760                    case 'NOTE':
761                        // Shared notes. Inline notes are handled elsewhere.
762                        echo ' data-autocomplete-type="NOTE"';
763                        break;
764                    case 'OBJE':
765                        echo ' data-autocomplete-type="OBJE"';
766                        break;
767                    case 'PAGE':
768                        echo ' data-autocomplete-type="PAGE" data-autocomplete-extra="#' . $source_element_id . '"';
769                        break;
770                    case 'PLAC':
771                        echo ' data-autocomplete-type="PLAC"';
772                        break;
773                    case 'REPO':
774                        echo ' data-autocomplete-type="REPO"';
775                        break;
776                    case 'SOUR':
777                        $source_element_id = $element_id;
778                        echo ' data-autocomplete-type="SOUR"';
779                        break;
780                    case 'SURN':
781                    case '_MARNM_SURN':
782                        echo ' data-autocomplete-type="SURN"';
783                        break;
784                    case 'TIME':
785                        echo ' pattern="([0-1][0-9]|2[0-3]):[0-5][0-9](:[0-5][0-9])?" dir="ltr" placeholder="' . /* I18N: Examples of valid time formats (hours:minutes:seconds) */
786                        I18N::translate('hh:mm or hh:mm:ss') . '"';
787                        break;
788                }
789                echo '>';
790            }
791
792            $tmp_array = array('TYPE', 'TIME', 'NOTE', 'SOUR', 'REPO', 'OBJE', 'ASSO', '_ASSO', 'AGE');
793
794            // split PLAC
795            if ($fact === 'PLAC') {
796                echo '<div id="', $element_id, '_pop" style="display: inline;">';
797                echo FunctionsPrint::printSpecialCharacterLink($element_id), ' ', FunctionsPrint::printFindPlaceLink($element_id);
798                echo '<span  onclick="jQuery(\'tr[id^=', $upperlevel, '_LATI],tr[id^=', $upperlevel, '_LONG],tr[id^=LATI],tr[id^=LONG]\').toggle(\'fast\'); return false;" class="icon-target" title="', GedcomTag::getLabel('LATI'), ' / ', GedcomTag::getLabel('LONG'), '"></span>';
799                echo '</div>';
800                if (Module::getModuleByName('places_assistant')) {
801                    \PlacesAssistantModule::setup_place_subfields($element_id);
802                    \PlacesAssistantModule::print_place_subfields($element_id);
803                }
804            } elseif (!in_array($fact, $tmp_array)) {
805                echo FunctionsPrint::printSpecialCharacterLink($element_id);
806            }
807        }
808        // MARRiage TYPE : hide text field and show a selection list
809        if ($fact === 'TYPE' && $level === 2 && $tags[0] === 'MARR') {
810            echo '<script>';
811            echo 'document.getElementById(\'', $element_id, '\').style.display=\'none\'';
812            echo '</script>';
813            echo '<select id="', $element_id, '_sel" onchange="document.getElementById(\'', $element_id, '\').value=this.value;" >';
814            foreach (array('Unknown', 'Civil', 'Religious', 'Partners') as $key) {
815                if ($key === 'Unknown') {
816                    echo '<option value="" ';
817                } else {
818                    echo '<option value="', $key, '" ';
819                }
820                $a = strtolower($key);
821                $b = strtolower($value);
822                if ($b !== '' && strpos($a, $b) !== false || strpos($b, $a) !== false) {
823                    echo 'selected';
824                }
825                echo '>', GedcomTag::getLabel('MARR_' . strtoupper($key)), '</option>';
826            }
827            echo '</select>';
828        } elseif ($fact === 'TYPE' && $level === 0) {
829            // NAME TYPE : hide text field and show a selection list
830            $onchange = 'onchange="document.getElementById(\'' . $element_id . '\').value=this.value;"';
831            echo self::editFieldNameType($element_name, $value, $onchange, $person);
832            echo '<script>document.getElementById("', $element_id, '").style.display="none";</script>';
833        }
834
835        // popup links
836        switch ($fact) {
837            case 'DATE':
838                echo self::printCalendarPopup($element_id);
839                break;
840            case 'FAMC':
841            case 'FAMS':
842                echo FunctionsPrint::printFindFamilyLink($element_id);
843                break;
844            case 'ALIA':
845            case 'ASSO':
846            case '_ASSO':
847                echo FunctionsPrint::printFindIndividualLink($element_id, $element_id . '_description');
848                break;
849            case 'FILE':
850                FunctionsPrint::printFindMediaLink($element_id, '0file');
851                break;
852            case 'SOUR':
853                echo FunctionsPrint::printFindSourceLink($element_id, $element_id . '_description'), ' ', self::printAddNewSourceLink($element_id);
854                //-- checkboxes to apply '1 SOUR' to BIRT/MARR/DEAT as '2 SOUR'
855                if ($level === 1) {
856                    echo '<br>';
857                    switch ($WT_TREE->getPreference('PREFER_LEVEL2_SOURCES')) {
858                        case '2': // records
859                            $level1_checked = 'checked';
860                            $level2_checked = '';
861                            break;
862                        case '1': // facts
863                            $level1_checked = '';
864                            $level2_checked = 'checked';
865                        break;
866                        case '0': // none
867                        default:
868                            $level1_checked = '';
869                            $level2_checked = '';
870                        break;
871                    }
872                    if (strpos($bdm, 'B') !== false) {
873                        echo ' <label><input type="checkbox" name="SOUR_INDI" ', $level1_checked, ' value="1">', I18N::translate('Individual'), '</label>';
874                        if (preg_match_all('/(' . WT_REGEX_TAG . ')/', $WT_TREE->getPreference('QUICK_REQUIRED_FACTS'), $matches)) {
875                            foreach ($matches[1] as $match) {
876                                if (!in_array($match, explode('|', WT_EVENTS_DEAT))) {
877                                    echo ' <label><input type="checkbox" name="SOUR_', $match, '" ', $level2_checked, ' value="1">', GedcomTag::getLabel($match), '</label>';
878                                }
879                            }
880                        }
881                    }
882                    if (strpos($bdm, 'D') !== false) {
883                        if (preg_match_all('/(' . WT_REGEX_TAG . ')/', $WT_TREE->getPreference('QUICK_REQUIRED_FACTS'), $matches)) {
884                            foreach ($matches[1] as $match) {
885                                if (in_array($match, explode('|', WT_EVENTS_DEAT))) {
886                                    echo ' <label><input type="checkbox" name="SOUR_', $match, '"', $level2_checked, ' value="1">', GedcomTag::getLabel($match), '</label>';
887                                }
888                            }
889                        }
890                    }
891                    if (strpos($bdm, 'M') !== false) {
892                        echo ' <label><input type="checkbox" name="SOUR_FAM" ', $level1_checked, ' value="1">', I18N::translate('Family'), '</label>';
893                        if (preg_match_all('/(' . WT_REGEX_TAG . ')/', $WT_TREE->getPreference('QUICK_REQUIRED_FAMFACTS'), $matches)) {
894                            foreach ($matches[1] as $match) {
895                                echo ' <label><input type="checkbox" name="SOUR_', $match, '"', $level2_checked, ' value="1">', GedcomTag::getLabel($match), '</label>';
896                            }
897                        }
898                    }
899                }
900                break;
901            case 'REPO':
902                echo FunctionsPrint::printFindRepositoryLink($element_id), ' ', self::printAddNewRepositoryLink($element_id);
903                break;
904            case 'NOTE':
905                // Shared Notes Icons ========================================
906                if ($islink) {
907                    // Print regular Shared Note icons ---------------------------
908                    echo ' ', FunctionsPrint::printFindNoteLink($element_id, $element_id . '_description'), ' ', self::printAddNewNoteLink($element_id);
909                    if ($value) {
910                        echo ' ', self::printEditNoteLink($value);
911                    }
912                }
913                break;
914            case 'OBJE':
915                echo FunctionsPrint::printFindMediaLink($element_id, '1media');
916                if (!$value) {
917                    echo ' ', self::printAddNewMediaLink($element_id);
918                    $value = 'new';
919                }
920                break;
921        }
922
923        echo '<div id="' . $element_id . '_description">';
924
925        // current value
926        if ($fact === 'DATE') {
927            $date = new Date($value);
928            echo $date->display();
929        }
930        if (($fact === 'ASSO' || $fact === '_ASSO') && $value === '') {
931            if ($level === 1) {
932                echo '<p class="small text-muted">' . I18N::translate('An associate is another individual who was involved with this individual, such as a friend or an employer.') . '</p>';
933            } else {
934                echo '<p class="small text-muted">' . I18N::translate('An associate is another individual who was involved with this fact or event, such as a witness or a priest.') . '</p>';
935            }
936        }
937
938        if ($value && $value !== 'new' && $islink) {
939            switch ($fact) {
940                case 'ALIA':
941                case 'ASSO':
942                case '_ASSO':
943                    $tmp = Individual::getInstance($value, $WT_TREE);
944                    if ($tmp) {
945                        echo ' ', $tmp->getFullName();
946                    }
947                    break;
948                case 'SOUR':
949                    $tmp = Source::getInstance($value, $WT_TREE);
950                    if ($tmp) {
951                        echo ' ', $tmp->getFullName();
952                    }
953                    break;
954                case 'NOTE':
955                    $tmp = Note::getInstance($value, $WT_TREE);
956                    if ($tmp) {
957                        echo ' ', $tmp->getFullName();
958                    }
959                    break;
960                case 'OBJE':
961                    $tmp = Media::getInstance($value, $WT_TREE);
962                    if ($tmp) {
963                        echo ' ', $tmp->getFullName();
964                    }
965                    break;
966                case 'REPO':
967                    $tmp = Repository::getInstance($value, $WT_TREE);
968                    if ($tmp) {
969                        echo ' ', $tmp->getFullName();
970                    }
971                    break;
972            }
973        }
974
975        // pastable values
976        if ($fact === 'FORM' && $upperlevel === 'OBJE') {
977            FunctionsPrint::printAutoPasteLink($element_id, Config::fileFormats());
978        }
979        echo '</div>', $extra, '</td></tr>';
980
981        return $element_id;
982    }
983
984    /**
985     * Genearate a <select> element, with the dates/places of all known censuses
986     *
987     * @param string $locale - Sort the censuses for this locale
988     * @param string $xref   - The individual for whom we are adding a census
989     *
990     * @return string
991     */
992    public static function censusDateSelector($locale, $xref)
993    {
994        global $controller;
995
996        // Show more likely census details at the top of the list.
997        switch (WT_LOCALE) {
998            case 'cs':
999                $census_places = array(new CensusOfCzechRepublic);
1000                break;
1001            case 'en-AU':
1002            case 'en-GB':
1003                $census_places = array(new CensusOfEngland, new CensusOfWales, new CensusOfScotland);
1004                break;
1005            case 'en-US':
1006                $census_places = array(new CensusOfUnitedStates);
1007                break;
1008            case 'fr':
1009            case 'fr-CA':
1010                $census_places = array(new CensusOfFrance);
1011                break;
1012            case 'da':
1013                $census_places = array(new CensusOfDenmark);
1014                break;
1015            case 'de':
1016                $census_places = array(new CensusOfDeutschland);
1017                break;
1018            default:
1019                $census_places = array();
1020                break;
1021        }
1022        foreach (Census::allCensusPlaces() as $census_place) {
1023            if (!in_array($census_place, $census_places)) {
1024                $census_places[] = $census_place;
1025            }
1026        }
1027
1028        $controller->addInlineJavascript('
1029				function selectCensus(el) {
1030					var option = jQuery(":selected", el);
1031					jQuery("input.DATE", jQuery(el).closest("table")).val(option.val());
1032					jQuery("input.PLAC", jQuery(el).closest("table")).val(option.data("place"));
1033					jQuery("input.census-class", jQuery(el).closest("table")).val(option.data("census"));
1034					if (option.data("place")) {
1035						jQuery("#assistant-link").show();
1036					} else {
1037						jQuery("#assistant-link").hide();
1038					}
1039				}
1040				function set_pid_array(pa) {
1041					jQuery("#pid_array").val(pa);
1042				}
1043				function activateCensusAssistant() {
1044					if (jQuery("#newshared_note_img").hasClass("icon-plus")) {
1045						expand_layer("newshared_note");
1046					}
1047					var field  = jQuery("#newshared_note input.NOTE")[0];
1048					var xref   = jQuery("input[name=xref]").val();
1049					var census = jQuery(".census-assistant-selector :selected").data("census");
1050					return addnewnote_assisted(field, xref, census);
1051				}
1052			');
1053
1054        $options = '<option value="">' . I18N::translate('Census date') . '</option>';
1055
1056        foreach ($census_places as $census_place) {
1057            $options .= '<option value=""></option>';
1058            foreach ($census_place->allCensusDates() as $census) {
1059                $date            = new Date($census->censusDate());
1060                $year            = $date->minimumDate()->format('%Y');
1061                $place_hierarchy = explode(', ', $census->censusPlace());
1062                $options .= '<option value="' . $census->censusDate() . '" data-place="' . $census->censusPlace() . '" data-census="' . get_class($census) . '">' . $place_hierarchy[0] . ' ' . $year . '</option>';
1063            }
1064        }
1065
1066        return
1067            '<input type="hidden" id="pid_array" name="pid_array" value="">' .
1068            '<select class="census-assistant-selector" onchange="selectCensus(this);">' . $options . '</select>';
1069    }
1070
1071    /**
1072     * Prints collapsable fields to add ASSO/RELA, SOUR, OBJE, etc.
1073     *
1074     * @param string $tag
1075     * @param int $level
1076     * @param string $parent_tag
1077     */
1078    public static function printAddLayer($tag, $level = 2, $parent_tag = '')
1079    {
1080        global $WT_TREE;
1081
1082        switch ($tag) {
1083            case 'SOUR':
1084                echo '<a href="#" onclick="return expand_layer(\'newsource\');"><i id="newsource_img" class="icon-plus"></i> ', I18N::translate('Add a source citation'), '</a>';
1085                echo '<br>';
1086                echo '<div id="newsource" style="display: none;">';
1087                echo '<table class="facts_table">';
1088                // 2 SOUR
1089                self::addSimpleTag($level . ' SOUR @');
1090                // 3 PAGE
1091                self::addSimpleTag(($level + 1) . ' PAGE');
1092                // 3 DATA
1093                self::addSimpleTag(($level + 1) . ' DATA');
1094                // 4 TEXT
1095                self::addSimpleTag(($level + 2) . ' TEXT');
1096                if ($WT_TREE->getPreference('FULL_SOURCES')) {
1097                    // 4 DATE
1098                    self::addSimpleTag(($level + 2) . ' DATE', '', GedcomTag::getLabel('DATA:DATE'));
1099                    // 3 QUAY
1100                    self::addSimpleTag(($level + 1) . ' QUAY');
1101                }
1102                // 3 OBJE
1103                self::addSimpleTag(($level + 1) . ' OBJE');
1104                // 3 SHARED_NOTE
1105                self::addSimpleTag(($level + 1) . ' SHARED_NOTE');
1106                echo '</table></div>';
1107                break;
1108
1109            case 'ASSO':
1110            case 'ASSO2':
1111                //-- Add a new ASSOciate
1112                if ($tag === 'ASSO') {
1113                    echo "<a href=\"#\" onclick=\"return expand_layer('newasso');\"><i id=\"newasso_img\" class=\"icon-plus\"></i> ", I18N::translate('Add an associate'), '</a>';
1114                    echo '<br>';
1115                    echo '<div id="newasso" style="display: none;">';
1116                } else {
1117                    echo "<a href=\"#\" onclick=\"return expand_layer('newasso2');\"><i id=\"newasso2_img\" class=\"icon-plus\"></i> ", I18N::translate('Add an associate'), '</a>';
1118                    echo '<br>';
1119                    echo '<div id="newasso2" style="display: none;">';
1120                }
1121                echo '<table class="facts_table">';
1122                // 2 ASSO
1123                self::addSimpleTag($level . ' _ASSO @');
1124                // 3 RELA
1125                self::addSimpleTag(($level + 1) . ' RELA');
1126                // 3 NOTE
1127                self::addSimpleTag(($level + 1) . ' NOTE');
1128                // 3 SHARED_NOTE
1129                self::addSimpleTag(($level + 1) . ' SHARED_NOTE');
1130                echo '</table></div>';
1131                break;
1132
1133            case 'NOTE':
1134                //-- Retrieve existing note or add new note to fact
1135                echo "<a href=\"#\" onclick=\"return expand_layer('newnote');\"><i id=\"newnote_img\" class=\"icon-plus\"></i> ", I18N::translate('Add a note'), '</a>';
1136                echo '<br>';
1137                echo '<div id="newnote" style="display: none;">';
1138                echo '<table class="facts_table">';
1139                // 2 NOTE
1140                self::addSimpleTag($level . ' NOTE');
1141                echo '</table></div>';
1142                break;
1143
1144            case 'SHARED_NOTE':
1145                echo "<a href=\"#\" onclick=\"return expand_layer('newshared_note');\"><i id=\"newshared_note_img\" class=\"icon-plus\"></i> ", I18N::translate('Add a shared note'), '</a>';
1146                echo '<br>';
1147                echo '<div id="newshared_note" style="display: none;">';
1148                echo '<table class="facts_table">';
1149                // 2 SHARED NOTE
1150                self::addSimpleTag($level . ' SHARED_NOTE', $parent_tag);
1151                echo '</table></div>';
1152                break;
1153
1154            case 'OBJE':
1155                if ($WT_TREE->getPreference('MEDIA_UPLOAD') >= Auth::accessLevel($WT_TREE)) {
1156                    echo "<a href=\"#\" onclick=\"return expand_layer('newobje');\"><i id=\"newobje_img\" class=\"icon-plus\"></i> ", I18N::translate('Add a media object'), '</a>';
1157                    echo '<br>';
1158                    echo '<div id="newobje" style="display: none;">';
1159                    echo '<table class="facts_table">';
1160                    self::addSimpleTag($level . ' OBJE');
1161                    echo '</table></div>';
1162                }
1163                break;
1164
1165            case 'RESN':
1166                echo "<a href=\"#\" onclick=\"return expand_layer('newresn');\"><i id=\"newresn_img\" class=\"icon-plus\"></i> ", I18N::translate('Add a restriction'), '</a>';
1167                echo '<br>';
1168                echo '<div id="newresn" style="display: none;">';
1169                echo '<table class="facts_table">';
1170                // 2 RESN
1171                self::addSimpleTag($level . ' RESN');
1172                echo '</table></div>';
1173                break;
1174        }
1175    }
1176
1177    /**
1178     * Add some empty tags to create a new fact.
1179     *
1180     * @param string $fact
1181     */
1182    public static function addSimpleTags($fact)
1183    {
1184        global $WT_TREE;
1185
1186        // For new individuals, these facts default to "Y"
1187        if ($fact === 'MARR') {
1188            self::addSimpleTag('0 ' . $fact . ' Y');
1189        } else {
1190            self::addSimpleTag('0 ' . $fact);
1191        }
1192
1193        if (!in_array($fact, Config::nonDateFacts())) {
1194            self::addSimpleTag('0 DATE', $fact, GedcomTag::getLabel($fact . ':DATE'));
1195        }
1196
1197        if (!in_array($fact, Config::nonPlaceFacts())) {
1198            self::addSimpleTag('0 PLAC', $fact, GedcomTag::getLabel($fact . ':PLAC'));
1199
1200            if (preg_match_all('/(' . WT_REGEX_TAG . ')/', $WT_TREE->getPreference('ADVANCED_PLAC_FACTS'), $match)) {
1201                foreach ($match[1] as $tag) {
1202                    self::addSimpleTag('0 ' . $tag, $fact, GedcomTag::getLabel($fact . ':PLAC:' . $tag));
1203                }
1204            }
1205            self::addSimpleTag('0 MAP', $fact);
1206            self::addSimpleTag('0 LATI', $fact);
1207            self::addSimpleTag('0 LONG', $fact);
1208        }
1209    }
1210
1211    /**
1212     * Assemble the pieces of a newly created record into gedcom
1213     *
1214     * @return string
1215     */
1216    public static function addNewName()
1217    {
1218        global $WT_TREE;
1219
1220        $gedrec = "\n1 NAME " . Filter::post('NAME');
1221
1222        $tags = array('NPFX', 'GIVN', 'SPFX', 'SURN', 'NSFX');
1223
1224        if (preg_match_all('/(' . WT_REGEX_TAG . ')/', $WT_TREE->getPreference('ADVANCED_NAME_FACTS'), $match)) {
1225            $tags = array_merge($tags, $match[1]);
1226        }
1227
1228        // Paternal and Polish and Lithuanian surname traditions can also create a _MARNM
1229        $SURNAME_TRADITION = $WT_TREE->getPreference('SURNAME_TRADITION');
1230        if ($SURNAME_TRADITION === 'paternal' || $SURNAME_TRADITION === 'polish' || $SURNAME_TRADITION === 'lithuanian') {
1231            $tags[] = '_MARNM';
1232        }
1233
1234        foreach (array_unique($tags) as $tag) {
1235            $TAG = Filter::post($tag);
1236            if ($TAG) {
1237                $gedrec .= "\n2 {$tag} {$TAG}";
1238            }
1239        }
1240
1241        return $gedrec;
1242    }
1243
1244    /**
1245     * Create a form to add a sex record.
1246     *
1247     * @return string
1248     */
1249    public static function addNewSex()
1250    {
1251        switch (Filter::post('SEX', '[MF]', 'U')) {
1252            case 'M':
1253                return "\n1 SEX M";
1254            case 'F':
1255                return "\n1 SEX F";
1256            default:
1257                return "\n1 SEX U";
1258        }
1259    }
1260
1261    /**
1262     * Create a form to add a new fact.
1263     *
1264     * @param string $fact
1265     *
1266     * @return string
1267     */
1268    public static function addNewFact($fact)
1269    {
1270        global $WT_TREE;
1271
1272        $FACT = Filter::post($fact);
1273        $DATE = Filter::post($fact . '_DATE');
1274        $PLAC = Filter::post($fact . '_PLAC');
1275        if ($DATE || $PLAC || $FACT && $FACT !== 'Y') {
1276            if ($FACT && $FACT !== 'Y') {
1277                $gedrec = "\n1 " . $fact . ' ' . $FACT;
1278            } else {
1279                $gedrec = "\n1 " . $fact;
1280            }
1281            if ($DATE) {
1282                $gedrec .= "\n2 DATE " . $DATE;
1283            }
1284            if ($PLAC) {
1285                $gedrec .= "\n2 PLAC " . $PLAC;
1286
1287                if (preg_match_all('/(' . WT_REGEX_TAG . ')/', $WT_TREE->getPreference('ADVANCED_PLAC_FACTS'), $match)) {
1288                    foreach ($match[1] as $tag) {
1289                        $TAG = Filter::post($fact . '_' . $tag);
1290                        if ($TAG) {
1291                            $gedrec .= "\n3 " . $tag . ' ' . $TAG;
1292                        }
1293                    }
1294                }
1295                $LATI = Filter::post($fact . '_LATI');
1296                $LONG = Filter::post($fact . '_LONG');
1297                if ($LATI || $LONG) {
1298                    $gedrec .= "\n3 MAP\n4 LATI " . $LATI . "\n4 LONG " . $LONG;
1299                }
1300            }
1301            if (Filter::postBool('SOUR_' . $fact)) {
1302                return self::updateSource($gedrec, 2);
1303            } else {
1304                return $gedrec;
1305            }
1306        } elseif ($FACT === 'Y') {
1307            if (Filter::postBool('SOUR_' . $fact)) {
1308                return self::updateSource("\n1 " . $fact . ' Y', 2);
1309            } else {
1310                return "\n1 " . $fact . ' Y';
1311            }
1312        } else {
1313            return '';
1314        }
1315    }
1316
1317    /**
1318     * This function splits the $glevels, $tag, $islink, and $text arrays so that the
1319     * entries associated with a SOUR record are separate from everything else.
1320     *
1321     * Input arrays:
1322     * - $glevels[] - an array of the gedcom level for each line that was edited
1323     * - $tag[] - an array of the tags for each gedcom line that was edited
1324     * - $islink[] - an array of 1 or 0 values to indicate when the text is a link element
1325     * - $text[] - an array of the text data for each line
1326     *
1327     * Output arrays:
1328     * ** For the SOUR record:
1329     * - $glevelsSOUR[] - an array of the gedcom level for each line that was edited
1330     * - $tagSOUR[] - an array of the tags for each gedcom line that was edited
1331     * - $islinkSOUR[] - an array of 1 or 0 values to indicate when the text is a link element
1332     * - $textSOUR[] - an array of the text data for each line
1333     * ** For the remaining records:
1334     * - $glevelsRest[] - an array of the gedcom level for each line that was edited
1335     * - $tagRest[] - an array of the tags for each gedcom line that was edited
1336     * - $islinkRest[] - an array of 1 or 0 values to indicate when the text is a link element
1337     * - $textRest[] - an array of the text data for each line
1338     */
1339    public static function splitSource()
1340    {
1341        global $glevels, $tag, $islink, $text;
1342        global $glevelsSOUR, $tagSOUR, $islinkSOUR, $textSOUR;
1343        global $glevelsRest, $tagRest, $islinkRest, $textRest;
1344
1345        $glevelsSOUR = array();
1346        $tagSOUR     = array();
1347        $islinkSOUR  = array();
1348        $textSOUR    = array();
1349
1350        $glevelsRest = array();
1351        $tagRest     = array();
1352        $islinkRest  = array();
1353        $textRest    = array();
1354
1355        $inSOUR = false;
1356
1357        for ($i = 0; $i < count($glevels); $i++) {
1358            if ($inSOUR) {
1359                if ($levelSOUR < $glevels[$i]) {
1360                    $dest = 'S';
1361                } else {
1362                    $inSOUR = false;
1363                    $dest   = 'R';
1364                }
1365            } else {
1366                if ($tag[$i] === 'SOUR') {
1367                    $inSOUR    = true;
1368                    $levelSOUR = $glevels[$i];
1369                    $dest      = 'S';
1370                } else {
1371                    $dest = 'R';
1372                }
1373            }
1374            if ($dest === 'S') {
1375                $glevelsSOUR[] = $glevels[$i];
1376                $tagSOUR[]     = $tag[$i];
1377                $islinkSOUR[]  = $islink[$i];
1378                $textSOUR[]    = $text[$i];
1379            } else {
1380                $glevelsRest[] = $glevels[$i];
1381                $tagRest[]     = $tag[$i];
1382                $islinkRest[]  = $islink[$i];
1383                $textRest[]    = $text[$i];
1384            }
1385        }
1386    }
1387
1388    /**
1389     * Add new GEDCOM lines from the $xxxSOUR interface update arrays, which
1390     * were produced by the splitSOUR() function.
1391     * See the FunctionsEdit::handle_updatesges() function for details.
1392     *
1393     * @param string $inputRec
1394     * @param string $levelOverride
1395     *
1396     * @return string
1397     */
1398    public static function updateSource($inputRec, $levelOverride = 'no')
1399    {
1400        global $glevels, $tag, $islink, $text;
1401        global $glevelsSOUR, $tagSOUR, $islinkSOUR, $textSOUR;
1402
1403        if (count($tagSOUR) === 0) {
1404            return $inputRec; // No update required
1405        }
1406
1407        // Save original interface update arrays before replacing them with the xxxSOUR ones
1408        $glevelsSave = $glevels;
1409        $tagSave     = $tag;
1410        $islinkSave  = $islink;
1411        $textSave    = $text;
1412
1413        $glevels = $glevelsSOUR;
1414        $tag     = $tagSOUR;
1415        $islink  = $islinkSOUR;
1416        $text    = $textSOUR;
1417
1418        $myRecord = self::handleUpdates($inputRec, $levelOverride); // Now do the update
1419
1420        // Restore the original interface update arrays (just in case ...)
1421        $glevels = $glevelsSave;
1422        $tag     = $tagSave;
1423        $islink  = $islinkSave;
1424        $text    = $textSave;
1425
1426        return $myRecord;
1427    }
1428
1429    /**
1430     * Add new GEDCOM lines from the $xxxRest interface update arrays, which
1431     * were produced by the splitSOUR() function.
1432     * See the FunctionsEdit::handle_updatesges() function for details.
1433     *
1434     * @param string $inputRec
1435     * @param string $levelOverride
1436     *
1437     * @return string
1438     */
1439    public static function updateRest($inputRec, $levelOverride = 'no')
1440    {
1441        global $glevels, $tag, $islink, $text;
1442        global $glevelsRest, $tagRest, $islinkRest, $textRest;
1443
1444        if (count($tagRest) === 0) {
1445            return $inputRec; // No update required
1446        }
1447
1448        // Save original interface update arrays before replacing them with the xxxRest ones
1449        $glevelsSave = $glevels;
1450        $tagSave     = $tag;
1451        $islinkSave  = $islink;
1452        $textSave    = $text;
1453
1454        $glevels = $glevelsRest;
1455        $tag     = $tagRest;
1456        $islink  = $islinkRest;
1457        $text    = $textRest;
1458
1459        $myRecord = self::handleUpdates($inputRec, $levelOverride); // Now do the update
1460
1461        // Restore the original interface update arrays (just in case ...)
1462        $glevels = $glevelsSave;
1463        $tag     = $tagSave;
1464        $islink  = $islinkSave;
1465        $text    = $textSave;
1466
1467        return $myRecord;
1468    }
1469
1470    /**
1471     * Add new gedcom lines from interface update arrays
1472     * The edit_interface and FunctionsEdit::add_simple_tag function produce the following
1473     * arrays incoming from the $_POST form
1474     * - $glevels[] - an array of the gedcom level for each line that was edited
1475     * - $tag[] - an array of the tags for each gedcom line that was edited
1476     * - $islink[] - an array of 1 or 0 values to tell whether the text is a link element and should be surrounded by @@
1477     * - $text[] - an array of the text data for each line
1478     * With these arrays you can recreate the gedcom lines like this
1479     * <code>$glevel[0].' '.$tag[0].' '.$text[0]</code>
1480     * There will be an index in each of these arrays for each line of the gedcom
1481     * fact that is being edited.
1482     * If the $text[] array is empty for the given line, then it means that the
1483     * user removed that line during editing or that the line is supposed to be
1484     * empty (1 DEAT, 1 BIRT) for example. To know if the line should be removed
1485     * there is a section of code that looks ahead to the next lines to see if there
1486     * are sub lines. For example we don't want to remove the 1 DEAT line if it has
1487     * a 2 PLAC or 2 DATE line following it. If there are no sub lines, then the line
1488     * can be safely removed.
1489     *
1490     * @param string $newged the new gedcom record to add the lines to
1491     * @param string $levelOverride Override GEDCOM level specified in $glevels[0]
1492     *
1493     * @return string The updated gedcom record
1494     */
1495    public static function handleUpdates($newged, $levelOverride = 'no')
1496    {
1497        global $glevels, $islink, $tag, $uploaded_files, $text;
1498
1499        if ($levelOverride === 'no' || count($glevels) === 0) {
1500            $levelAdjust = 0;
1501        } else {
1502            $levelAdjust = $levelOverride - $glevels[0];
1503        }
1504
1505        for ($j = 0; $j < count($glevels); $j++) {
1506
1507            // Look for empty SOUR reference with non-empty sub-records.
1508            // This can happen when the SOUR entry is deleted but its sub-records
1509            // were incorrectly left intact.
1510            // The sub-records should be deleted.
1511            if ($tag[$j] === 'SOUR' && ($text[$j] === '@@' || $text[$j] === '')) {
1512                $text[$j] = '';
1513                $k        = $j + 1;
1514                while (($k < count($glevels)) && ($glevels[$k] > $glevels[$j])) {
1515                    $text[$k] = '';
1516                    $k++;
1517                }
1518            }
1519
1520            if (trim($text[$j]) !== '') {
1521                $pass = true;
1522            } else {
1523                //-- for facts with empty values they must have sub records
1524                //-- this section checks if they have subrecords
1525                $k    = $j + 1;
1526                $pass = false;
1527                while (($k < count($glevels)) && ($glevels[$k] > $glevels[$j])) {
1528                    if ($text[$k] !== '') {
1529                        if (($tag[$j] !== 'OBJE') || ($tag[$k] === 'FILE')) {
1530                            $pass = true;
1531                            break;
1532                        }
1533                    }
1534                    if (($tag[$k] === 'FILE') && (count($uploaded_files) > 0)) {
1535                        $filename = array_shift($uploaded_files);
1536                        if (!empty($filename)) {
1537                            $text[$k] = $filename;
1538                            $pass     = true;
1539                            break;
1540                        }
1541                    }
1542                    $k++;
1543                }
1544            }
1545
1546            //-- if the value is not empty or it has sub lines
1547            //--- then write the line to the gedcom record
1548            //-- we have to let some emtpy text lines pass through... (DEAT, BIRT, etc)
1549            if ($pass) {
1550                $newline = $glevels[$j] + $levelAdjust . ' ' . $tag[$j];
1551                if ($text[$j] !== '') {
1552                    if ($islink[$j]) {
1553                        $newline .= ' @' . $text[$j] . '@';
1554                    } else {
1555                        $newline .= ' ' . $text[$j];
1556                    }
1557                }
1558                $newged .= "\n" . str_replace("\n", "\n" . (1 + substr($newline, 0, 1)) . ' CONT ', $newline);
1559            }
1560        }
1561
1562        return $newged;
1563    }
1564
1565    /**
1566     * builds the form for adding new facts
1567     *
1568     * @param string $fact the new fact we are adding
1569     */
1570    public static function createAddForm($fact)
1571    {
1572        global $tags, $WT_TREE;
1573
1574        $tags = array();
1575
1576        // handle  MARRiage TYPE
1577        if (substr($fact, 0, 5) === 'MARR_') {
1578            $tags[0] = 'MARR';
1579            self::addSimpleTag('1 MARR');
1580            self::insertMissingSubtags($fact);
1581        } else {
1582            $tags[0] = $fact;
1583            if ($fact === '_UID') {
1584                $fact .= ' ' . GedcomTag::createUid();
1585            }
1586            // These new level 1 tags need to be turned into links
1587            if (in_array($fact, array('ALIA', 'ASSO'))) {
1588                $fact .= ' @';
1589            }
1590            if (in_array($fact, Config::emptyFacts())) {
1591                self::addSimpleTag('1 ' . $fact . ' Y');
1592            } else {
1593                self::addSimpleTag('1 ' . $fact);
1594            }
1595            self::insertMissingSubtags($tags[0]);
1596            //-- handle the special SOURce case for level 1 sources [ 1759246 ]
1597            if ($fact === 'SOUR') {
1598                self::addSimpleTag('2 PAGE');
1599                self::addSimpleTag('2 DATA');
1600                self::addSimpleTag('3 TEXT');
1601                if ($WT_TREE->getPreference('FULL_SOURCES')) {
1602                    self::addSimpleTag('3 DATE', '', GedcomTag::getLabel('DATA:DATE'));
1603                    self::addSimpleTag('2 QUAY');
1604                }
1605            }
1606        }
1607    }
1608
1609    /**
1610     * Create a form to edit a Fact object.
1611     *
1612     * @param Fact $fact
1613     *
1614     * @return string
1615     */
1616    public static function createEditForm(Fact $fact)
1617    {
1618        global $tags;
1619
1620        $record = $fact->getParent();
1621
1622        $tags     = array();
1623        $gedlines = explode("\n", $fact->getGedcom());
1624
1625        $linenum = 0;
1626        $fields  = explode(' ', $gedlines[$linenum]);
1627        $glevel  = $fields[0];
1628        $level   = $glevel;
1629
1630        $type       = $fact->getTag();
1631        $level0type = $record::RECORD_TYPE;
1632        $level1type = $type;
1633
1634        $i           = $linenum;
1635        $inSource    = false;
1636        $levelSource = 0;
1637        $add_date    = true;
1638
1639        // List of tags we would expect at the next level
1640        // NB add_missing_subtags() already takes care of the simple cases
1641        // where a level 1 tag is missing a level 2 tag. Here we only need to
1642        // handle the more complicated cases.
1643        $expected_subtags = array(
1644            'SOUR' => array('PAGE', 'DATA'),
1645            'DATA' => array('TEXT'),
1646            'PLAC' => array('MAP'),
1647            'MAP'  => array('LATI', 'LONG'),
1648        );
1649        if ($record->getTree()->getPreference('FULL_SOURCES')) {
1650            $expected_subtags['SOUR'][] = 'QUAY';
1651            $expected_subtags['DATA'][] = 'DATE';
1652        }
1653        if (GedcomCodeTemp::isTagLDS($level1type)) {
1654            $expected_subtags['STAT'] = array('DATE');
1655        }
1656        if (in_array($level1type, Config::dateAndTime())) {
1657            $expected_subtags['DATE'] = array('TIME'); // TIME is NOT a valid 5.5.1 tag
1658        }
1659        if (preg_match_all('/(' . WT_REGEX_TAG . ')/', $record->getTree()->getPreference('ADVANCED_PLAC_FACTS'), $match)) {
1660            $expected_subtags['PLAC'] = array_merge($match[1], $expected_subtags['PLAC']);
1661        }
1662
1663        $stack = array();
1664        // Loop on existing tags :
1665        while (true) {
1666            // Keep track of our hierarchy, e.g. 1=>BIRT, 2=>PLAC, 3=>FONE
1667            $stack[$level] = $type;
1668            // Merge them together, e.g. BIRT:PLAC:FONE
1669            $label = implode(':', array_slice($stack, 0, $level));
1670
1671            $text = '';
1672            for ($j = 2; $j < count($fields); $j++) {
1673                if ($j > 2) {
1674                    $text .= ' ';
1675                }
1676                $text .= $fields[$j];
1677            }
1678            $text = rtrim($text);
1679            while (($i + 1 < count($gedlines)) && (preg_match("/" . ($level + 1) . ' CONT ?(.*)/', $gedlines[$i + 1], $cmatch) > 0)) {
1680                $text .= "\n" . $cmatch[1];
1681                $i++;
1682            }
1683
1684            if ($type === 'SOUR') {
1685                $inSource    = true;
1686                $levelSource = $level;
1687            } elseif ($levelSource >= $level) {
1688                $inSource = false;
1689            }
1690
1691            if ($type !== 'CONT') {
1692                $tags[]    = $type;
1693                $subrecord = $level . ' ' . $type . ' ' . $text;
1694                if ($inSource && $type === 'DATE') {
1695                    self::addSimpleTag($subrecord, '', GedcomTag::getLabel($label, $record));
1696                } elseif (!$inSource && $type === 'DATE') {
1697                    self::addSimpleTag($subrecord, $level1type, GedcomTag::getLabel($label, $record));
1698                    if ($level === '2') {
1699                        // We already have a date - no need to add one.
1700                        $add_date = false;
1701                    }
1702                } elseif ($type === 'STAT') {
1703                    self::addSimpleTag($subrecord, $level1type, GedcomTag::getLabel($label, $record));
1704                } else {
1705                    self::addSimpleTag($subrecord, $level0type, GedcomTag::getLabel($label, $record));
1706                }
1707            }
1708
1709            // Get a list of tags present at the next level
1710            $subtags = array();
1711            for ($ii = $i + 1; isset($gedlines[$ii]) && preg_match('/^(\d+) (\S+)/', $gedlines[$ii], $mm) && $mm[1] > $level; ++$ii) {
1712                if ($mm[1] == $level + 1) {
1713                    $subtags[] = $mm[2];
1714                }
1715            }
1716
1717            // Insert missing tags
1718            if (!empty($expected_subtags[$type])) {
1719                foreach ($expected_subtags[$type] as $subtag) {
1720                    if (!in_array($subtag, $subtags)) {
1721                        self::addSimpleTag(($level + 1) . ' ' . $subtag, '', GedcomTag::getLabel($label . ':' . $subtag));
1722                        if (!empty($expected_subtags[$subtag])) {
1723                            foreach ($expected_subtags[$subtag] as $subsubtag) {
1724                                self::addSimpleTag(($level + 2) . ' ' . $subsubtag, '', GedcomTag::getLabel($label . ':' . $subtag . ':' . $subsubtag));
1725                            }
1726                        }
1727                    }
1728                }
1729            }
1730
1731            $i++;
1732            if (isset($gedlines[$i])) {
1733                $fields = explode(' ', $gedlines[$i]);
1734                $level  = $fields[0];
1735                if (isset($fields[1])) {
1736                    $type = trim($fields[1]);
1737                } else {
1738                    $level = 0;
1739                }
1740            } else {
1741                $level = 0;
1742            }
1743            if ($level <= $glevel) {
1744                break;
1745            }
1746        }
1747
1748        if ($level1type !== '_PRIM') {
1749            self::insertMissingSubtags($level1type, $add_date);
1750        }
1751
1752        return $level1type;
1753    }
1754
1755    /**
1756     * Populates the global $tags array with any missing sub-tags.
1757     *
1758     * @param string $level1tag the type of the level 1 gedcom record
1759     * @param bool $add_date
1760     */
1761    public static function insertMissingSubtags($level1tag, $add_date = false)
1762    {
1763        global $tags, $WT_TREE;
1764
1765        // handle  MARRiage TYPE
1766        $type_val = '';
1767        if (substr($level1tag, 0, 5) === 'MARR_') {
1768            $type_val  = substr($level1tag, 5);
1769            $level1tag = 'MARR';
1770        }
1771
1772        foreach (Config::levelTwoTags() as $key => $value) {
1773            if ($key === 'DATE' && in_array($level1tag, Config::nonDateFacts()) || $key === 'PLAC' && in_array($level1tag, Config::nonPlaceFacts())) {
1774                continue;
1775            }
1776            if (in_array($level1tag, $value) && !in_array($key, $tags)) {
1777                if ($key === 'TYPE') {
1778                    self::addSimpleTag('2 TYPE ' . $type_val, $level1tag);
1779                } elseif ($level1tag === '_TODO' && $key === 'DATE') {
1780                    self::addSimpleTag('2 ' . $key . ' ' . strtoupper(date('d M Y')), $level1tag);
1781                } elseif ($level1tag === '_TODO' && $key === '_WT_USER') {
1782                    self::addSimpleTag('2 ' . $key . ' ' . Auth::user()->getUserName(), $level1tag);
1783                } elseif ($level1tag === 'TITL' && strstr($WT_TREE->getPreference('ADVANCED_NAME_FACTS'), $key) !== false) {
1784                    self::addSimpleTag('2 ' . $key, $level1tag);
1785                } elseif ($level1tag === 'NAME' && strstr($WT_TREE->getPreference('ADVANCED_NAME_FACTS'), $key) !== false) {
1786                    self::addSimpleTag('2 ' . $key, $level1tag);
1787                } elseif ($level1tag !== 'TITL' && $level1tag !== 'NAME') {
1788                    self::addSimpleTag('2 ' . $key, $level1tag);
1789                }
1790                // Add level 3/4 tags as appropriate
1791                switch ($key) {
1792                    case 'PLAC':
1793                        if (preg_match_all('/(' . WT_REGEX_TAG . ')/', $WT_TREE->getPreference('ADVANCED_PLAC_FACTS'), $match)) {
1794                            foreach ($match[1] as $tag) {
1795                                self::addSimpleTag('3 ' . $tag, '', GedcomTag::getLabel($level1tag . ':PLAC:' . $tag));
1796                            }
1797                        }
1798                        self::addSimpleTag('3 MAP');
1799                        self::addSimpleTag('4 LATI');
1800                        self::addSimpleTag('4 LONG');
1801                        break;
1802                    case 'FILE':
1803                        self::addSimpleTag('3 FORM');
1804                        break;
1805                    case 'EVEN':
1806                        self::addSimpleTag('3 DATE');
1807                        self::addSimpleTag('3 PLAC');
1808                        break;
1809                    case 'STAT':
1810                        if (GedcomCodeTemp::isTagLDS($level1tag)) {
1811                            self::addSimpleTag('3 DATE', '', GedcomTag::getLabel('STAT:DATE'));
1812                        }
1813                        break;
1814                    case 'DATE':
1815                        // TIME is NOT a valid 5.5.1 tag
1816                        if (in_array($level1tag, Config::dateAndTime())) {
1817                            self::addSimpleTag('3 TIME');
1818                        }
1819                        break;
1820                    case 'HUSB':
1821                    case 'WIFE':
1822                        self::addSimpleTag('3 AGE');
1823                        break;
1824                    case 'FAMC':
1825                        if ($level1tag === 'ADOP') {
1826                            self::addSimpleTag('3 ADOP BOTH');
1827                        }
1828                        break;
1829                }
1830            } elseif ($key === 'DATE' && $add_date) {
1831                self::addSimpleTag('2 DATE', $level1tag, GedcomTag::getLabel($level1tag . ':DATE'));
1832            }
1833        }
1834        // Do something (anything!) with unrecognized custom tags
1835        if (substr($level1tag, 0, 1) === '_' && $level1tag !== '_UID' && $level1tag !== '_TODO') {
1836            foreach (array('DATE', 'PLAC', 'ADDR', 'AGNC', 'TYPE', 'AGE') as $tag) {
1837                if (!in_array($tag, $tags)) {
1838                    self::addSimpleTag('2 ' . $tag);
1839                    if ($tag === 'PLAC') {
1840                        if (preg_match_all('/(' . WT_REGEX_TAG . ')/', $WT_TREE->getPreference('ADVANCED_PLAC_FACTS'), $match)) {
1841                            foreach ($match[1] as $ptag) {
1842                                self::addSimpleTag('3 ' . $ptag, '', GedcomTag::getLabel($level1tag . ':PLAC:' . $ptag));
1843                            }
1844                        }
1845                        self::addSimpleTag('3 MAP');
1846                        self::addSimpleTag('4 LATI');
1847                        self::addSimpleTag('4 LONG');
1848                    }
1849                }
1850            }
1851        }
1852    }
1853}
1854