xref: /webtrees/app/Services/GedcomEditService.php (revision 842a873c0b92c28d4af3fa9db0e9156057005569)
1<?php
2
3/**
4 * webtrees: online genealogy
5 * Copyright (C) 2021 webtrees development team
6 * This program is free software: you can redistribute it and/or modify
7 * it under the terms of the GNU General Public License as published by
8 * the Free Software Foundation, either version 3 of the License, or
9 * (at your option) any later version.
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
14 * You should have received a copy of the GNU General Public License
15 * along with this program. If not, see <https://www.gnu.org/licenses/>.
16 */
17
18declare(strict_types=1);
19
20namespace Fisharebest\Webtrees\Services;
21
22use Fisharebest\Webtrees\Gedcom;
23use Fisharebest\Webtrees\Tree;
24use Psr\Http\Message\ServerRequestInterface;
25
26use function array_merge;
27use function array_unique;
28use function assert;
29use function count;
30use function preg_match_all;
31use function str_replace;
32use function trim;
33
34/**
35 * Utilities to edit/save GEDCOM data.
36 */
37class GedcomEditService
38{
39    /** @var string[] */
40    public $glevels = [];
41
42    /** @var string[] */
43    public $tag = [];
44
45    /** @var string[] */
46    public $islink = [];
47
48    /** @var string[] */
49    public $text = [];
50
51    /** @var string[] */
52    protected $glevelsSOUR = [];
53
54    /** @var string[] */
55    protected $tagSOUR = [];
56
57    /** @var string[] */
58    protected $islinkSOUR = [];
59
60    /** @var string[] */
61    protected $textSOUR = [];
62
63    /** @var string[] */
64    protected $glevelsRest = [];
65
66    /** @var string[] */
67    protected $tagRest = [];
68
69    /** @var string[] */
70    protected $islinkRest = [];
71
72    /** @var string[] */
73    protected $textRest = [];
74
75    /**
76     * This function splits the $glevels, $tag, $islink, and $text arrays so that the
77     * entries associated with a SOUR record are separate from everything else.
78     *
79     * Input arrays:
80     * - $glevels[] - an array of the gedcom level for each line that was edited
81     * - $tag[] - an array of the tags for each gedcom line that was edited
82     * - $islink[] - an array of 1 or 0 values to indicate when the text is a link element
83     * - $text[] - an array of the text data for each line
84     *
85     * Output arrays:
86     * ** For the SOUR record:
87     * - $glevelsSOUR[] - an array of the gedcom level for each line that was edited
88     * - $tagSOUR[] - an array of the tags for each gedcom line that was edited
89     * - $islinkSOUR[] - an array of 1 or 0 values to indicate when the text is a link element
90     * - $textSOUR[] - an array of the text data for each line
91     * ** For the remaining records:
92     * - $glevelsRest[] - an array of the gedcom level for each line that was edited
93     * - $tagRest[] - an array of the tags for each gedcom line that was edited
94     * - $islinkRest[] - an array of 1 or 0 values to indicate when the text is a link element
95     * - $textRest[] - an array of the text data for each line
96     *
97     * @return void
98     */
99    public function splitSource(): void
100    {
101        $this->glevelsSOUR = [];
102        $this->tagSOUR     = [];
103        $this->islinkSOUR  = [];
104        $this->textSOUR    = [];
105
106        $this->glevelsRest = [];
107        $this->tagRest     = [];
108        $this->islinkRest  = [];
109        $this->textRest    = [];
110
111        $inSOUR    = false;
112        $levelSOUR = 0;
113
114        // Assume all arrays are the same size.
115        $count = count($this->glevels);
116
117        for ($i = 0; $i < $count; $i++) {
118            if ($inSOUR) {
119                if ($levelSOUR < $this->glevels[$i]) {
120                    $dest = 'S';
121                } else {
122                    $inSOUR = false;
123                    $dest   = 'R';
124                }
125            } elseif ($this->tag[$i] === 'SOUR') {
126                $inSOUR    = true;
127                $levelSOUR = $this->glevels[$i];
128                $dest      = 'S';
129            } else {
130                $dest = 'R';
131            }
132
133            if ($dest === 'S') {
134                $this->glevelsSOUR[] = $this->glevels[$i];
135                $this->tagSOUR[]     = $this->tag[$i];
136                $this->islinkSOUR[]  = $this->islink[$i];
137                $this->textSOUR[]    = $this->text[$i];
138            } else {
139                $this->glevelsRest[] = $this->glevels[$i];
140                $this->tagRest[]     = $this->tag[$i];
141                $this->islinkRest[]  = $this->islink[$i];
142                $this->textRest[]    = $this->text[$i];
143            }
144        }
145    }
146
147    /**
148     * Add new GEDCOM lines from the $xxxRest interface update arrays, which
149     * were produced by the splitSOUR() function.
150     * See the FunctionsEdit::handle_updatesges() function for details.
151     *
152     * @param string $inputRec
153     *
154     * @return string
155     */
156    public function updateRest(string $inputRec): string
157    {
158        if (count($this->tagRest) === 0) {
159            return $inputRec; // No update required
160        }
161
162        // Save original interface update arrays before replacing them with the xxxRest ones
163        $glevelsSave = $this->glevels;
164        $tagSave     = $this->tag;
165        $islinkSave  = $this->islink;
166        $textSave    = $this->text;
167
168        $this->glevels = $this->glevelsRest;
169        $this->tag     = $this->tagRest;
170        $this->islink  = $this->islinkRest;
171        $this->text    = $this->textRest;
172
173        $myRecord = $this->handleUpdates($inputRec, 'no'); // Now do the update
174
175        // Restore the original interface update arrays (just in case ...)
176        $this->glevels = $glevelsSave;
177        $this->tag     = $tagSave;
178        $this->islink  = $islinkSave;
179        $this->text    = $textSave;
180
181        return $myRecord;
182    }
183
184    /**
185     * Add new gedcom lines from interface update arrays
186     * The edit_interface and FunctionsEdit::add_simple_tag function produce the following
187     * arrays incoming from the $_POST form
188     * - $glevels[] - an array of the gedcom level for each line that was edited
189     * - $tag[] - an array of the tags for each gedcom line that was edited
190     * - $islink[] - an array of 1 or 0 values to tell whether the text is a link element and should be surrounded by @@
191     * - $text[] - an array of the text data for each line
192     * With these arrays you can recreate the gedcom lines like this
193     * <code>$glevel[0].' '.$tag[0].' '.$text[0]</code>
194     * There will be an index in each of these arrays for each line of the gedcom
195     * fact that is being edited.
196     * If the $text[] array is empty for the given line, then it means that the
197     * user removed that line during editing or that the line is supposed to be
198     * empty (1 DEAT, 1 BIRT) for example. To know if the line should be removed
199     * there is a section of code that looks ahead to the next lines to see if there
200     * are sub lines. For example we don't want to remove the 1 DEAT line if it has
201     * a 2 PLAC or 2 DATE line following it. If there are no sub lines, then the line
202     * can be safely removed.
203     *
204     * @param string $newged        the new gedcom record to add the lines to
205     * @param string $levelOverride Override GEDCOM level specified in $glevels[0]
206     *
207     * @return string The updated gedcom record
208     */
209    public function handleUpdates(string $newged, $levelOverride = 'no'): string
210    {
211        if ($levelOverride === 'no') {
212            $levelAdjust = 0;
213        } else {
214            $levelAdjust = 1;
215        }
216
217        // Assert all arrays are the same size.
218        assert(count($this->glevels) === count($this->tag));
219        assert(count($this->glevels) === count($this->text));
220        assert(count($this->glevels) === count($this->islink));
221
222        $count = count($this->glevels);
223
224        for ($j = 0; $j < $count; $j++) {
225            // Look for empty SOUR reference with non-empty sub-records.
226            // This can happen when the SOUR entry is deleted but its sub-records
227            // were incorrectly left intact.
228            // The sub-records should be deleted.
229            if ($this->tag[$j] === 'SOUR' && ($this->text[$j] === '@@' || $this->text[$j] === '')) {
230                $this->text[$j] = '';
231                $k              = $j + 1;
232                while ($k < $count && $this->glevels[$k] > $this->glevels[$j]) {
233                    $this->text[$k] = '';
234                    $k++;
235                }
236            }
237
238            if (trim($this->text[$j]) !== '') {
239                $pass = true;
240            } else {
241                //-- for facts with empty values they must have sub records
242                //-- this section checks if they have subrecords
243                $k    = $j + 1;
244                $pass = false;
245                while ($k < $count && $this->glevels[$k] > $this->glevels[$j]) {
246                    if ($this->text[$k] !== '') {
247                        if (($this->tag[$j] !== 'OBJE') || ($this->tag[$k] === 'FILE')) {
248                            $pass = true;
249                            break;
250                        }
251                    }
252                    $k++;
253                }
254            }
255
256            //-- if the value is not empty or it has sub lines
257            //--- then write the line to the gedcom record
258            //-- we have to let some emtpy text lines pass through... (DEAT, BIRT, etc)
259            if ($pass) {
260                $newline = (int) $this->glevels[$j] + $levelAdjust . ' ' . $this->tag[$j];
261                if ($this->text[$j] !== '') {
262                    if ($this->islink[$j]) {
263                        $newline .= ' @' . $this->text[$j] . '@';
264                    } else {
265                        $newline .= ' ' . $this->text[$j];
266                    }
267                }
268                $next_level = 1 + (int) $this->glevels[$j] + $levelAdjust;
269
270                $newged .= "\n" . str_replace("\n", "\n" . $next_level . ' CONT ', $newline);
271            }
272        }
273
274        return $newged;
275    }
276
277    /**
278     * Create a form to add a new fact.
279     *
280     * @param ServerRequestInterface $request
281     * @param Tree                   $tree
282     * @param string                 $fact
283     *
284     * @return string
285     */
286    public function addNewFact(ServerRequestInterface $request, Tree $tree, $fact): string
287    {
288        $params = (array) $request->getParsedBody();
289
290        $FACT = $params[$fact];
291        $DATE = $params[$fact . '_DATE'] ?? '';
292        $PLAC = $params[$fact . '_PLAC'] ?? '';
293
294        if ($DATE !== '' || $PLAC !== '' || $FACT !== '' && $FACT !== 'Y') {
295            if ($FACT !== '' && $FACT !== 'Y') {
296                $gedrec = "\n1 " . $fact . ' ' . $FACT;
297            } else {
298                $gedrec = "\n1 " . $fact;
299            }
300            if ($DATE !== '') {
301                $gedrec .= "\n2 DATE " . $DATE;
302            }
303            if ($PLAC !== '') {
304                $gedrec .= "\n2 PLAC " . $PLAC;
305
306                if (preg_match_all('/(' . Gedcom::REGEX_TAG . ')/', $tree->getPreference('ADVANCED_PLAC_FACTS'), $match)) {
307                    foreach ($match[1] as $tag) {
308                        $TAG = $params[$fact . '_' . $tag];
309                        if ($TAG !== '') {
310                            $gedrec .= "\n3 " . $tag . ' ' . $TAG;
311                        }
312                    }
313                }
314                $LATI = $params[$fact . '_LATI'] ?? '';
315                $LONG = $params[$fact . '_LONG'] ?? '';
316                if ($LATI !== '' || $LONG !== '') {
317                    $gedrec .= "\n3 MAP\n4 LATI " . $LATI . "\n4 LONG " . $LONG;
318                }
319            }
320            if ((bool) ($params['SOUR_' . $fact] ?? false)) {
321                return $this->updateSource($gedrec, 'yes');
322            }
323
324            return $gedrec;
325        }
326
327        if ($FACT === 'Y') {
328            if ((bool) ($params['SOUR_' . $fact] ?? false)) {
329                return $this->updateSource("\n1 " . $fact . ' Y', 'yes');
330            }
331
332            return "\n1 " . $fact . ' Y';
333        }
334
335        return '';
336    }
337
338    /**
339     * Add new GEDCOM lines from the $xxxSOUR interface update arrays, which
340     * were produced by the splitSOUR() function.
341     * See the FunctionsEdit::handle_updatesges() function for details.
342     *
343     * @param string $inputRec
344     * @param string $levelOverride
345     *
346     * @return string
347     */
348    public function updateSource(string $inputRec, string $levelOverride = 'no'): string
349    {
350        if (count($this->tagSOUR) === 0) {
351            return $inputRec; // No update required
352        }
353
354        // Save original interface update arrays before replacing them with the xxxSOUR ones
355        $glevelsSave = $this->glevels;
356        $tagSave     = $this->tag;
357        $islinkSave  = $this->islink;
358        $textSave    = $this->text;
359
360        $this->glevels = $this->glevelsSOUR;
361        $this->tag     = $this->tagSOUR;
362        $this->islink  = $this->islinkSOUR;
363        $this->text    = $this->textSOUR;
364
365        $myRecord = $this->handleUpdates($inputRec, $levelOverride); // Now do the update
366
367        // Restore the original interface update arrays (just in case ...)
368        $this->glevels = $glevelsSave;
369        $this->tag     = $tagSave;
370        $this->islink  = $islinkSave;
371        $this->text    = $textSave;
372
373        return $myRecord;
374    }
375
376    /**
377     * Create a form to add a sex record.
378     *
379     * @param ServerRequestInterface $request
380     *
381     * @return string
382     */
383    public function addNewSex(ServerRequestInterface $request): string
384    {
385        $params = (array) $request->getParsedBody();
386
387        switch ($params['SEX']) {
388            case 'M':
389                return "\n1 SEX M";
390            case 'F':
391                return "\n1 SEX F";
392            default:
393                return "\n1 SEX U";
394        }
395    }
396
397    /**
398     * Assemble the pieces of a newly created record into gedcom
399     *
400     * @param ServerRequestInterface $request
401     * @param Tree                   $tree
402     *
403     * @return string
404     */
405    public function addNewName(ServerRequestInterface $request, Tree $tree): string
406    {
407        $params = (array) $request->getParsedBody();
408        $gedrec = "\n1 NAME " . $params['NAME'];
409
410        $tags = [
411            'NPFX',
412            'GIVN',
413            'SPFX',
414            'SURN',
415            'NSFX',
416            'NICK',
417        ];
418
419        if (preg_match_all('/(' . Gedcom::REGEX_TAG . ')/', $tree->getPreference('ADVANCED_NAME_FACTS'), $match)) {
420            $tags = array_merge($tags, $match[1]);
421        }
422
423        // Paternal and Polish and Lithuanian surname traditions can also create a _MARNM
424        $SURNAME_TRADITION = $tree->getPreference('SURNAME_TRADITION');
425        if ($SURNAME_TRADITION === 'paternal' || $SURNAME_TRADITION === 'polish' || $SURNAME_TRADITION === 'lithuanian') {
426            $tags[] = '_MARNM';
427        }
428
429        foreach (array_unique($tags) as $tag) {
430            $TAG = $params[$tag];
431
432            if ($TAG !== '') {
433                $gedrec .= "\n2 " . $tag . ' ' . $TAG;
434            }
435        }
436
437        return $gedrec;
438    }
439}
440