Completed
Push — develop ( 90766b...23de6e )
by Greg
15:33 queued 06:18
created

GedcomEditService::editLinesToGedcom()   C

Complexity

Conditions 15
Paths 19

Size

Total Lines 47
Code Lines 26

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 15
eloc 26
c 0
b 0
f 0
nc 19
nop 4
dl 0
loc 47
rs 5.9166

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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