Passed
Push — master ( bcc125...6f8ef0 )
by Greg
05:44
created

FunctionsImport   F

Complexity

Total Complexity 123

Size/Duplication

Total Lines 838
Duplicated Lines 0 %

Importance

Changes 6
Bugs 1 Features 0
Metric Value
eloc 477
dl 0
loc 838
rs 2
c 6
b 1
f 0
wmc 123

11 Methods

Rating   Name   Duplication   Size   Complexity  
A convertInlineMedia() 0 16 4
F importRecord() 0 192 28
B updateRecord() 0 90 9
B createMediaObject() 0 56 6
F reformatRecord() 0 176 53
B importLegacyPlacDefn() 0 30 6
A updateLinks() 0 18 2
A updateNames() 0 40 4
A updatePlaces() 0 28 3
A importTNGPlac() 0 28 6
A updateDates() 0 41 2

How to fix   Complexity   

Complex Class

Complex classes like FunctionsImport often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use FunctionsImport, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
/**
4
 * webtrees: online genealogy
5
 * Copyright (C) 2020 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 <http://www.gnu.org/licenses/>.
16
 */
17
18
declare(strict_types=1);
19
20
namespace Fisharebest\Webtrees\Functions;
21
22
use Fisharebest\Webtrees\Date;
23
use Fisharebest\Webtrees\Exceptions\GedcomErrorException;
24
use Fisharebest\Webtrees\Factory;
25
use Fisharebest\Webtrees\Family;
26
use Fisharebest\Webtrees\Gedcom;
27
use Fisharebest\Webtrees\GedcomTag;
28
use Fisharebest\Webtrees\Header;
29
use Fisharebest\Webtrees\Individual;
30
use Fisharebest\Webtrees\Location;
31
use Fisharebest\Webtrees\Media;
32
use Fisharebest\Webtrees\Note;
33
use Fisharebest\Webtrees\Place;
34
use Fisharebest\Webtrees\PlaceLocation;
35
use Fisharebest\Webtrees\Repository;
36
use Fisharebest\Webtrees\Services\GedcomService;
37
use Fisharebest\Webtrees\Soundex;
38
use Fisharebest\Webtrees\Source;
39
use Fisharebest\Webtrees\Submission;
40
use Fisharebest\Webtrees\Submitter;
41
use Fisharebest\Webtrees\Tree;
42
use Illuminate\Database\Capsule\Manager as DB;
43
use Illuminate\Database\Query\JoinClause;
44
45
use function app;
46
use function array_intersect_key;
47
use function array_map;
48
use function array_unique;
49
use function assert;
50
use function date;
51
use function explode;
52
use function max;
53
use function preg_match;
54
use function preg_match_all;
55
use function preg_replace;
56
use function round;
57
use function str_contains;
58
use function str_replace;
59
use function str_starts_with;
60
use function strlen;
61
use function strtolower;
62
use function strtoupper;
63
use function substr;
64
use function trim;
65
66
use const PREG_SET_ORDER;
67
68
/**
69
 * Class FunctionsImport - common functions
70
 */
71
class FunctionsImport
72
{
73
    /**
74
     * Tidy up a gedcom record on import, so that we can access it consistently/efficiently.
75
     *
76
     * @param string $rec
77
     * @param Tree   $tree
78
     *
79
     * @return string
80
     */
81
    public static function reformatRecord(string $rec, Tree $tree): string
82
    {
83
        $gedcom_service = app(GedcomService::class);
84
        assert($gedcom_service instanceof GedcomService);
85
86
        // Strip out mac/msdos line endings
87
        $rec = preg_replace("/[\r\n]+/", "\n", $rec);
88
89
        // Extract lines from the record; lines consist of: level + optional xref + tag + optional data
90
        $num_matches = preg_match_all('/^[ \t]*(\d+)[ \t]*(@[^@]*@)?[ \t]*(\w+)[ \t]?(.*)$/m', $rec, $matches, PREG_SET_ORDER);
91
92
        // Process the record line-by-line
93
        $newrec = '';
94
        foreach ($matches as $n => $match) {
95
            [, $level, $xref, $tag, $data] = $match;
96
97
            $tag = $gedcom_service->canonicalTag($tag);
98
99
            switch ($tag) {
100
                case 'AFN':
101
                    // AFN values are upper case
102
                    $data = strtoupper($data);
103
                    break;
104
                case 'DATE':
105
                    // Preserve text from INT dates
106
                    if (str_contains($data, '(')) {
0 ignored issues
show
Deprecated Code introduced by
The function str_contains() has been deprecated: Str::contains() should be used directly instead. Will be removed in Laravel 6.0. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

106
                    if (/** @scrutinizer ignore-deprecated */ str_contains($data, '(')) {

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
107
                        [$date, $text] = explode('(', $data, 2);
108
                        $text = ' (' . $text;
109
                    } else {
110
                        $date = $data;
111
                        $text = '';
112
                    }
113
                    // Capitals
114
                    $date = strtoupper($date);
115
                    // Temporarily add leading/trailing spaces, to allow efficient matching below
116
                    $date = " {$date} ";
117
                    // Ensure space digits and letters
118
                    $date = preg_replace('/([A-Z])(\d)/', '$1 $2', $date);
119
                    $date = preg_replace('/(\d)([A-Z])/', '$1 $2', $date);
120
                    // Ensure space before/after calendar escapes
121
                    $date = preg_replace('/@#[^@]+@/', ' $0 ', $date);
122
                    // "BET." => "BET"
123
                    $date = preg_replace('/(\w\w)\./', '$1', $date);
124
                    // "CIR" => "ABT"
125
                    $date = str_replace(' CIR ', ' ABT ', $date);
126
                    $date = str_replace(' APX ', ' ABT ', $date);
127
                    // B.C. => BC (temporarily, to allow easier handling of ".")
128
                    $date = str_replace(' B.C. ', ' BC ', $date);
129
                    // TMG uses "EITHER X OR Y"
130
                    $date = preg_replace('/^ EITHER (.+) OR (.+)/', ' BET $1 AND $2', $date);
131
                    // "BET X - Y " => "BET X AND Y"
132
                    $date = preg_replace('/^(.* BET .+) - (.+)/', '$1 AND $2', $date);
133
                    $date = preg_replace('/^(.* FROM .+) - (.+)/', '$1 TO $2', $date);
134
                    // "@#ESC@ FROM X TO Y" => "FROM @#ESC@ X TO @#ESC@ Y"
135
                    $date = preg_replace('/^ +(@#[^@]+@) +FROM +(.+) +TO +(.+)/', ' FROM $1 $2 TO $1 $3', $date);
136
                    $date = preg_replace('/^ +(@#[^@]+@) +BET +(.+) +AND +(.+)/', ' BET $1 $2 AND $1 $3', $date);
137
                    // "@#ESC@ AFT X" => "AFT @#ESC@ X"
138
                    $date = preg_replace('/^ +(@#[^@]+@) +(FROM|BET|TO|AND|BEF|AFT|CAL|EST|INT|ABT) +(.+)/', ' $2 $1 $3', $date);
139
                    // Ignore any remaining punctuation, e.g. "14-MAY, 1900" => "14 MAY 1900"
140
                    // (don't change "/" - it is used in NS/OS dates)
141
                    $date = preg_replace('/[.,:;-]/', ' ', $date);
142
                    // BC => B.C.
143
                    $date = str_replace(' BC ', ' B.C. ', $date);
144
                    // Append the "INT" text
145
                    $data = $date . $text;
146
                    break;
147
                case '_FILE':
148
                    $tag = 'FILE';
149
                    break;
150
                case 'FORM':
151
                    // Consistent commas
152
                    $data = preg_replace('/ *, */', ', ', $data);
153
                    break;
154
                case 'HEAD':
155
                    // HEAD records don't have an XREF or DATA
156
                    if ($level === '0') {
157
                        $xref = '';
158
                        $data = '';
159
                    }
160
                    break;
161
                case 'NAME':
162
                    // Tidy up non-printing characters
163
                    $data = preg_replace('/  +/', ' ', trim($data));
164
                    break;
165
                case 'PEDI':
166
                    // PEDI values are lower case
167
                    $data = strtolower($data);
168
                    break;
169
                case 'PLAC':
170
                    // Consistent commas
171
                    $data = preg_replace('/ *[,,،] */u', ', ', $data);
172
                    // The Master Genealogist stores LAT/LONG data in the PLAC field, e.g. Pennsylvania, USA, 395945N0751013W
173
                    if (preg_match('/(.*), (\d\d)(\d\d)(\d\d)([NS])(\d\d\d)(\d\d)(\d\d)([EW])$/', $data, $match)) {
174
                        $data =
175
                            $match[1] . "\n" .
176
                            ($level + 1) . " MAP\n" .
177
                            ($level + 2) . ' LATI ' . ($match[5] . round($match[2] + ($match[3] / 60) + ($match[4] / 3600), 4)) . "\n" .
178
                            ($level + 2) . ' LONG ' . ($match[9] . round($match[6] + ($match[7] / 60) + ($match[8] / 3600), 4));
179
                    }
180
                    break;
181
                case 'RESN':
182
                    // RESN values are lower case (confidential, privacy, locked, none)
183
                    $data = strtolower($data);
184
                    if ($data === 'invisible') {
185
                        $data = 'confidential'; // From old versions of Legacy.
186
                    }
187
                    break;
188
                case 'SEX':
189
                    $data = strtoupper($data);
190
                    break;
191
                case 'STAT':
192
                    if ($data === 'CANCELLED') {
193
                        // PhpGedView mis-spells this tag - correct it.
194
                        $data = 'CANCELED';
195
                    }
196
                    break;
197
                case 'TEMP':
198
                    // Temple codes are upper case
199
                    $data = strtoupper($data);
200
                    break;
201
                case 'TRLR':
202
                    // TRLR records don't have an XREF or DATA
203
                    if ($level === '0') {
204
                        $xref = '';
205
                        $data = '';
206
                    }
207
                    break;
208
            }
209
            // Suppress "Y", for facts/events with a DATE or PLAC
210
            if ($data === 'y') {
211
                $data = 'Y';
212
            }
213
            if ($level === '1' && $data === 'Y') {
214
                for ($i = $n + 1; $i < $num_matches - 1 && $matches[$i][1] !== '1'; ++$i) {
215
                    if ($matches[$i][3] === 'DATE' || $matches[$i][3] === 'PLAC') {
216
                        $data = '';
217
                        break;
218
                    }
219
                }
220
            }
221
            // Reassemble components back into a single line
222
            switch ($tag) {
223
                default:
224
                    // Remove tabs and multiple/leading/trailing spaces
225
                    $data = strtr($data, ["\t" => ' ']);
226
                    $data = trim($data, ' ');
227
                    while (str_contains($data, '  ')) {
0 ignored issues
show
Deprecated Code introduced by
The function str_contains() has been deprecated: Str::contains() should be used directly instead. Will be removed in Laravel 6.0. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

227
                    while (/** @scrutinizer ignore-deprecated */ str_contains($data, '  ')) {

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
228
                        $data = strtr($data, ['  ' => ' ']);
229
                    }
230
                    $newrec .= ($newrec ? "\n" : '') . $level . ' ' . ($level === '0' && $xref ? $xref . ' ' : '') . $tag . ($data === '' && $tag !== 'NOTE' ? '' : ' ' . $data);
231
                    break;
232
                case 'NOTE':
233
                case 'TEXT':
234
                case 'DATA':
235
                case 'CONT':
236
                    $newrec .= ($newrec ? "\n" : '') . $level . ' ' . ($level === '0' && $xref ? $xref . ' ' : '') . $tag . ($data === '' && $tag !== 'NOTE' ? '' : ' ' . $data);
237
                    break;
238
                case 'FILE':
239
                    // Strip off the user-defined path prefix
240
                    $GEDCOM_MEDIA_PATH = $tree->getPreference('GEDCOM_MEDIA_PATH');
241
                    if ($GEDCOM_MEDIA_PATH !== '' && str_starts_with($data, $GEDCOM_MEDIA_PATH)) {
242
                        $data = substr($data, strlen($GEDCOM_MEDIA_PATH));
243
                    }
244
                    // convert backslashes in filenames to forward slashes
245
                    $data = preg_replace("/\\\\/", '/', $data);
246
247
                    $newrec .= ($newrec ? "\n" : '') . $level . ' ' . ($level === '0' && $xref ? $xref . ' ' : '') . $tag . ($data === '' && $tag !== 'NOTE' ? '' : ' ' . $data);
248
                    break;
249
                case 'CONC':
250
                    // Merge CONC lines, to simplify access later on.
251
                    $newrec .= ($tree->getPreference('WORD_WRAPPED_NOTES') ? ' ' : '') . $data;
252
                    break;
253
            }
254
        }
255
256
        return $newrec;
257
    }
258
259
    /**
260
     * import record into database
261
     * this function will parse the given gedcom record and add it to the database
262
     *
263
     * @param string $gedrec the raw gedcom record to parse
264
     * @param Tree   $tree   import the record into this tree
265
     * @param bool   $update whether or not this is an updated record that has been accepted
266
     *
267
     * @return void
268
     * @throws GedcomErrorException
269
     */
270
    public static function importRecord(string $gedrec, Tree $tree, bool $update): void
271
    {
272
        $tree_id = $tree->id();
273
274
        // Escaped @ signs (only if importing from file)
275
        if (!$update) {
276
            $gedrec = str_replace('@@', '@', $gedrec);
277
        }
278
279
        // Standardise gedcom format
280
        $gedrec = self::reformatRecord($gedrec, $tree);
281
282
        // import different types of records
283
        if (preg_match('/^0 @(' . Gedcom::REGEX_XREF . ')@ (' . Gedcom::REGEX_TAG . ')/', $gedrec, $match)) {
284
            [, $xref, $type] = $match;
285
            // check for a _UID, if the record doesn't have one, add one
286
            if ($tree->getPreference('GENERATE_UIDS') === '1' && !str_contains($gedrec, "\n1 _UID ")) {
0 ignored issues
show
Deprecated Code introduced by
The function str_contains() has been deprecated: Str::contains() should be used directly instead. Will be removed in Laravel 6.0. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

286
            if ($tree->getPreference('GENERATE_UIDS') === '1' && !/** @scrutinizer ignore-deprecated */ str_contains($gedrec, "\n1 _UID ")) {

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
287
                $gedrec .= "\n1 _UID " . GedcomTag::createUid();
288
            }
289
        } elseif (preg_match('/0 (HEAD|TRLR|_PLAC |_PLAC_DEFN)/', $gedrec, $match)) {
290
            $type = $match[1];
291
            $xref = $type; // For records without an XREF, use the type as a pseudo XREF.
292
        } else {
293
            throw new GedcomErrorException($gedrec);
294
        }
295
296
        // If the user has downloaded their GEDCOM data (containing media objects) and edited it
297
        // using an application which does not support (and deletes) media objects, then add them
298
        // back in.
299
        if ($tree->getPreference('keep_media')) {
300
            $old_linked_media = DB::table('link')
301
                ->where('l_from', '=', $xref)
302
                ->where('l_file', '=', $tree_id)
303
                ->where('l_type', '=', 'OBJE')
304
                ->pluck('l_to');
305
306
            // Delete these links - so that we do not insert them again in updateLinks()
307
            DB::table('link')
308
                ->where('l_from', '=', $xref)
309
                ->where('l_file', '=', $tree_id)
310
                ->where('l_type', '=', 'OBJE')
311
                ->delete();
312
313
            foreach ($old_linked_media as $media_id) {
314
                $gedrec .= "\n1 OBJE @" . $media_id . '@';
315
            }
316
        }
317
318
        // Convert inline media into media objects
319
        $gedrec = self::convertInlineMedia($tree, $gedrec);
320
321
        switch ($type) {
322
            case Individual::RECORD_TYPE:
323
                $record = Factory::individual()->new($xref, $gedrec, null, $tree);
324
325
                if (preg_match('/\n1 RIN (.+)/', $gedrec, $match)) {
326
                    $rin = $match[1];
327
                } else {
328
                    $rin = $xref;
329
                }
330
331
                DB::table('individuals')->insert([
332
                    'i_id'     => $xref,
333
                    'i_file'   => $tree_id,
334
                    'i_rin'    => $rin,
335
                    'i_sex'    => $record->sex(),
336
                    'i_gedcom' => $gedrec,
337
                ]);
338
339
                // Update the cross-reference/index tables.
340
                self::updatePlaces($xref, $tree, $gedrec);
341
                self::updateDates($xref, $tree_id, $gedrec);
342
                self::updateNames($xref, $tree_id, $record);
343
                break;
344
345
            case Family::RECORD_TYPE:
346
                if (preg_match('/\n1 HUSB @(' . Gedcom::REGEX_XREF . ')@/', $gedrec, $match)) {
347
                    $husb = $match[1];
348
                } else {
349
                    $husb = '';
350
                }
351
                if (preg_match('/\n1 WIFE @(' . Gedcom::REGEX_XREF . ')@/', $gedrec, $match)) {
352
                    $wife = $match[1];
353
                } else {
354
                    $wife = '';
355
                }
356
                $nchi = preg_match_all('/\n1 CHIL @(' . Gedcom::REGEX_XREF . ')@/', $gedrec, $match);
357
                if (preg_match('/\n1 NCHI (\d+)/', $gedrec, $match)) {
358
                    $nchi = max($nchi, $match[1]);
359
                }
360
361
                DB::table('families')->insert([
362
                    'f_id'      => $xref,
363
                    'f_file'    => $tree_id,
364
                    'f_husb'    => $husb,
365
                    'f_wife'    => $wife,
366
                    'f_gedcom'  => $gedrec,
367
                    'f_numchil' => $nchi,
368
                ]);
369
370
                // Update the cross-reference/index tables.
371
                self::updatePlaces($xref, $tree, $gedrec);
372
                self::updateDates($xref, $tree_id, $gedrec);
373
                break;
374
375
            case Source::RECORD_TYPE:
376
                if (preg_match('/\n1 TITL (.+)/', $gedrec, $match)) {
377
                    $name = $match[1];
378
                } elseif (preg_match('/\n1 ABBR (.+)/', $gedrec, $match)) {
379
                    $name = $match[1];
380
                } else {
381
                    $name = $xref;
382
                }
383
384
                DB::table('sources')->insert([
385
                    's_id'     => $xref,
386
                    's_file'   => $tree_id,
387
                    's_name'   => mb_substr($name, 0, 255),
388
                    's_gedcom' => $gedrec,
389
                ]);
390
                break;
391
392
            case Repository::RECORD_TYPE:
393
            case Note::RECORD_TYPE:
394
            case Submission::RECORD_TYPE:
395
            case Submitter::RECORD_TYPE:
396
            case Location::RECORD_TYPE:
397
                DB::table('other')->insert([
398
                    'o_id'     => $xref,
399
                    'o_file'   => $tree_id,
400
                    'o_type'   => $type,
401
                    'o_gedcom' => $gedrec,
402
                ]);
403
                break;
404
405
            case Header::RECORD_TYPE:
406
                // Force HEAD records to have a creation date.
407
                if (!str_contains($gedrec, "\n1 DATE ")) {
0 ignored issues
show
Deprecated Code introduced by
The function str_contains() has been deprecated: Str::contains() should be used directly instead. Will be removed in Laravel 6.0. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

407
                if (!/** @scrutinizer ignore-deprecated */ str_contains($gedrec, "\n1 DATE ")) {

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
408
                    $today = strtoupper(date('d M Y'));
409
                    $gedrec .= "\n1 DATE " . $today;
410
                }
411
412
                DB::table('other')->insert([
413
                    'o_id'     => $xref,
414
                    'o_file'   => $tree_id,
415
                    'o_type'   => Header::RECORD_TYPE,
416
                    'o_gedcom' => $gedrec,
417
                ]);
418
                break;
419
420
421
            case Media::RECORD_TYPE:
422
                $record = Factory::media()->new($xref, $gedrec, null, $tree);
423
424
                DB::table('media')->insert([
425
                    'm_id'     => $xref,
426
                    'm_file'   => $tree_id,
427
                    'm_gedcom' => $gedrec,
428
                ]);
429
430
                foreach ($record->mediaFiles() as $media_file) {
431
                    DB::table('media_file')->insert([
432
                        'm_id'                 => $xref,
433
                        'm_file'               => $tree_id,
434
                        'multimedia_file_refn' => mb_substr($media_file->filename(), 0, 248),
435
                        'multimedia_format'    => mb_substr($media_file->format(), 0, 4),
436
                        'source_media_type'    => mb_substr($media_file->type(), 0, 15),
437
                        'descriptive_title'    => mb_substr($media_file->title(), 0, 248),
438
                    ]);
439
                }
440
                break;
441
442
            case '_PLAC ':
443
                self::importTNGPlac($gedrec);
444
                return;
445
446
            case '_PLAC_DEFN':
447
                self::importLegacyPlacDefn($gedrec);
448
                return;
449
450
            default: // Custom record types.
451
                DB::table('other')->insert([
452
                    'o_id'     => $xref,
453
                    'o_file'   => $tree_id,
454
                    'o_type'   => mb_substr($type, 0, 15),
455
                    'o_gedcom' => $gedrec,
456
                ]);
457
                break;
458
        }
459
460
        // Update the cross-reference/index tables.
461
        self::updateLinks($xref, $tree_id, $gedrec);
462
    }
463
464
    /**
465
     * Legacy Family Tree software generates _PLAC_DEFN records containing LAT/LONG values
466
     *
467
     * @param string $gedcom
468
     */
469
    private static function importLegacyPlacDefn(string $gedcom): void
470
    {
471
        $gedcom_service = new GedcomService();
472
473
        if (preg_match('/\n1 PLAC (.+)/', $gedcom, $match)) {
474
            $place_name = $match[1];
475
        } else {
476
            return;
477
        }
478
479
        if (preg_match('/\n3 LATI ([NS].+)/', $gedcom, $match)) {
480
            $latitude = $gedcom_service->readLatitude($match[1]);
481
        } else {
482
            return;
483
        }
484
485
        if (preg_match('/\n3 LONG ([EW].+)/', $gedcom, $match)) {
486
            $longitude = $gedcom_service->readLongitude($match[1]);
487
        } else {
488
            return;
489
        }
490
491
        $location = new PlaceLocation($place_name);
492
493
        if ($location->latitude() === 0.0 && $location->longitude() === 0.0) {
0 ignored issues
show
introduced by
The condition $location->latitude() === 0.0 is always false.
Loading history...
494
            DB::table('placelocation')
495
                ->where('pl_id', '=', $location->id())
496
                ->update([
497
                    'pl_lati' => $latitude,
498
                    'pl_long' => $longitude,
499
                ]);
500
        }
501
    }
502
503
    /**
504
     * Legacy Family Tree software generates _PLAC_DEFN records containing LAT/LONG values
505
     *
506
     * @param string $gedcom
507
     */
508
    private static function importTNGPlac(string $gedcom): void
509
    {
510
        if (preg_match('/^0 _PLAC (.+)/', $gedcom, $match)) {
511
            $place_name = $match[1];
512
        } else {
513
            return;
514
        }
515
516
        if (preg_match('/\n2 LATI (.+)/', $gedcom, $match)) {
517
            $latitude = (float) $match[1];
518
        } else {
519
            return;
520
        }
521
522
        if (preg_match('/\n2 LONG (.+)/', $gedcom, $match)) {
523
            $longitude = (float) $match[1];
524
        } else {
525
            return;
526
        }
527
528
        $location = new PlaceLocation($place_name);
529
530
        if ($location->latitude() === 0.0 && $location->longitude() === 0.0) {
0 ignored issues
show
introduced by
The condition $location->latitude() === 0.0 is always false.
Loading history...
531
            DB::table('placelocation')
532
                ->where('pl_id', '=', $location->id())
533
                ->update([
534
                    'pl_lati' => $latitude,
535
                    'pl_long' => $longitude,
536
                ]);
537
        }
538
    }
539
540
    /**
541
     * Extract all level 2 places from the given record and insert them into the places table
542
     *
543
     * @param string $xref
544
     * @param Tree   $tree
545
     * @param string $gedrec
546
     *
547
     * @return void
548
     */
549
    public static function updatePlaces(string $xref, Tree $tree, string $gedrec): void
550
    {
551
        // Insert all new rows together
552
        $rows = [];
553
554
        preg_match_all('/^[2-9] PLAC (.+)/m', $gedrec, $matches);
555
556
        $places = array_unique($matches[1]);
557
558
        foreach ($places as $place_name) {
559
            $place = new Place($place_name, $tree);
560
561
            // Calling Place::id() will create the entry in the database, if it doesn't already exist.
562
            while ($place->id() !== 0) {
563
                $rows[] = [
564
                    'pl_p_id' => $place->id(),
565
                    'pl_gid'  => $xref,
566
                    'pl_file' => $tree->id(),
567
                ];
568
569
                $place = $place->parent();
570
            }
571
        }
572
573
        // array_unique doesn't work with arrays of arrays
574
        $rows = array_intersect_key($rows, array_unique(array_map('serialize', $rows)));
575
576
        DB::table('placelinks')->insert($rows);
577
    }
578
579
    /**
580
     * Extract all the dates from the given record and insert them into the database.
581
     *
582
     * @param string $xref
583
     * @param int    $ged_id
584
     * @param string $gedrec
585
     *
586
     * @return void
587
     */
588
    public static function updateDates(string $xref, int $ged_id, string $gedrec): void
589
    {
590
        // Insert all new rows together
591
        $rows = [];
592
593
        preg_match_all("/\n1 (\w+).*(?:\n[2-9].*)*(?:\n2 DATE (.+))(?:\n[2-9].*)*/", $gedrec, $matches, PREG_SET_ORDER);
594
595
        foreach ($matches as $match) {
596
            $fact = $match[1];
597
            $date = new Date($match[2]);
598
            $rows[] = [
599
                'd_day'        => $date->minimumDate()->day,
600
                'd_month'      => $date->minimumDate()->format('%O'),
601
                'd_mon'        => $date->minimumDate()->month,
602
                'd_year'       => $date->minimumDate()->year,
603
                'd_julianday1' => $date->minimumDate()->minimumJulianDay(),
604
                'd_julianday2' => $date->minimumDate()->maximumJulianDay(),
605
                'd_fact'       => $fact,
606
                'd_gid'        => $xref,
607
                'd_file'       => $ged_id,
608
                'd_type'       => $date->minimumDate()->format('%@'),
609
            ];
610
611
            $rows[] = [
612
                'd_day'        => $date->maximumDate()->day,
613
                'd_month'      => $date->maximumDate()->format('%O'),
614
                'd_mon'        => $date->maximumDate()->month,
615
                'd_year'       => $date->maximumDate()->year,
616
                'd_julianday1' => $date->maximumDate()->minimumJulianDay(),
617
                'd_julianday2' => $date->maximumDate()->maximumJulianDay(),
618
                'd_fact'       => $fact,
619
                'd_gid'        => $xref,
620
                'd_file'       => $ged_id,
621
                'd_type'       => $date->minimumDate()->format('%@'),
622
            ];
623
        }
624
625
        // array_unique doesn't work with arrays of arrays
626
        $rows = array_intersect_key($rows, array_unique(array_map('serialize', $rows)));
627
628
        DB::table('dates')->insert($rows);
629
    }
630
631
    /**
632
     * Extract all the links from the given record and insert them into the database
633
     *
634
     * @param string $xref
635
     * @param int    $ged_id
636
     * @param string $gedrec
637
     *
638
     * @return void
639
     */
640
    public static function updateLinks(string $xref, int $ged_id, string $gedrec): void
641
    {
642
        // Insert all new rows together
643
        $rows = [];
644
645
        preg_match_all('/\n\d+ (' . Gedcom::REGEX_TAG . ') @(' . Gedcom::REGEX_XREF . ')@/', $gedrec, $matches, PREG_SET_ORDER);
646
647
        foreach ($matches as $match) {
648
            // Take care of "duplicates" that differ on case/collation, e.g. "SOUR @S1@" and "SOUR @s1@"
649
            $rows[$match[1] . strtoupper($match[2])] = [
650
                'l_from' => $xref,
651
                'l_to'   => $match[2],
652
                'l_type' => $match[1],
653
                'l_file' => $ged_id,
654
            ];
655
        }
656
657
        DB::table('link')->insert($rows);
658
    }
659
660
    /**
661
     * Extract all the names from the given record and insert them into the database.
662
     *
663
     * @param string     $xref
664
     * @param int        $ged_id
665
     * @param Individual $record
666
     *
667
     * @return void
668
     */
669
    public static function updateNames(string $xref, int $ged_id, Individual $record): void
670
    {
671
        // Insert all new rows together
672
        $rows = [];
673
674
        foreach ($record->getAllNames() as $n => $name) {
675
            if ($name['givn'] === '@P.N.') {
676
                $soundex_givn_std = null;
677
                $soundex_givn_dm  = null;
678
            } else {
679
                $soundex_givn_std = Soundex::russell($name['givn']);
680
                $soundex_givn_dm  = Soundex::daitchMokotoff($name['givn']);
681
            }
682
683
            if ($name['surn'] === '@N.N.') {
684
                $soundex_surn_std = null;
685
                $soundex_surn_dm  = null;
686
            } else {
687
                $soundex_surn_std = Soundex::russell($name['surname']);
688
                $soundex_surn_dm  = Soundex::daitchMokotoff($name['surname']);
689
            }
690
691
            $rows[] = [
692
                'n_file'             => $ged_id,
693
                'n_id'               => $xref,
694
                'n_num'              => $n,
695
                'n_type'             => $name['type'],
696
                'n_sort'             => mb_substr($name['sort'], 0, 255),
697
                'n_full'             => mb_substr($name['fullNN'], 0, 255),
698
                'n_surname'          => mb_substr($name['surname'], 0, 255),
699
                'n_surn'             => mb_substr($name['surn'], 0, 255),
700
                'n_givn'             => mb_substr($name['givn'], 0, 255),
701
                'n_soundex_givn_std' => $soundex_givn_std,
702
                'n_soundex_surn_std' => $soundex_surn_std,
703
                'n_soundex_givn_dm'  => $soundex_givn_dm,
704
                'n_soundex_surn_dm'  => $soundex_surn_dm,
705
            ];
706
        }
707
708
        DB::table('name')->insert($rows);
709
    }
710
711
    /**
712
     * Extract inline media data, and convert to media objects.
713
     *
714
     * @param Tree   $tree
715
     * @param string $gedcom
716
     *
717
     * @return string
718
     */
719
    public static function convertInlineMedia(Tree $tree, string $gedcom): string
720
    {
721
        while (preg_match('/\n1 OBJE(?:\n[2-9].+)+/', $gedcom, $match)) {
722
            $xref   = self::createMediaObject($match[0], $tree);
723
            $gedcom = strtr($gedcom, [$match[0] =>  "\n1 OBJE @" . $xref . '@']);
724
        }
725
        while (preg_match('/\n2 OBJE(?:\n[3-9].+)+/', $gedcom, $match)) {
726
            $xref   = self::createMediaObject($match[0], $tree);
727
            $gedcom = strtr($gedcom, [$match[0] =>  "\n2 OBJE @" . $xref . '@']);
728
        }
729
        while (preg_match('/\n3 OBJE(?:\n[4-9].+)+/', $gedcom, $match)) {
730
            $xref   = self::createMediaObject($match[0], $tree);
731
            $gedcom = strtr($gedcom, [$match[0] =>  "\n3 OBJE @" . $xref . '@']);
732
        }
733
734
        return $gedcom;
735
    }
736
737
    /**
738
     * Create a new media object, from inline media data.
739
     *
740
     * GEDCOM 5.5.1: +1 FILE / +2 FORM / +3 MEDI / +1 TITL
741
     * GEDCOM 5.5: +1 FORM / +1 TITL / +1 FILE
742
     *
743
     * Legacy generates: +1 FORM / +1 FILE / +1 TITL
744
     * RootsMagic generates: +1 FILE / +1 FORM / +1 TITL
745
     *
746
     * @param string $gedcom
747
     * @param Tree   $tree
748
     *
749
     * @return string
750
     */
751
    public static function createMediaObject(string $gedcom, Tree $tree): string
752
    {
753
        preg_match('/\n\d FILE (.+)/u', $gedcom, $match);
754
        $file = $match[1] ?? '';
755
756
        preg_match('/\n\d TITL (.+)/u', $gedcom, $match);
757
        $title = $match[1] ?? '';
758
759
        preg_match('/\n\d FORM (.+)/u', $gedcom, $match);
760
        $format = $match[1] ?? '';
761
762
        preg_match('/\n\d (?:MEDI|TYPE) (.+)/u', $gedcom, $match);
763
        $type = $match[1] ?? '';
764
765
        // Have we already created a media object with the same title/filename?
766
        $xref = DB::table('media_file')
767
            ->where('m_file', '=', $tree->id())
768
            ->where('descriptive_title', '=', mb_substr($title, 0, 248))
769
            ->where('multimedia_file_refn', '=', mb_substr($file, 0, 248))
770
            ->value('m_id');
771
772
        if ($xref === null && $file !== '') {
773
            $xref = Factory::xref()->make(Media::RECORD_TYPE);
774
775
            // convert to a media-object
776
            $gedcom = '0 OBJE @' . $xref . "@\n1 FILE " . $file;
777
778
            if ($format !== '') {
779
                $gedcom .= "\n2 FORM " . $format;
780
781
                if ($type !== '') {
782
                    $gedcom .= "\n3 TYPE " . $type;
783
                }
784
            }
785
786
            if ($title !== '') {
787
                $gedcom .= "\n3 TITL " . $title;
788
            }
789
790
            DB::table('media')->insert([
791
                'm_id'     => $xref,
792
                'm_file'   => $tree->id(),
793
                'm_gedcom' => $gedcom,
794
            ]);
795
796
            DB::table('media_file')->insert([
797
                'm_id'                 => $xref,
798
                'm_file'               => $tree->id(),
799
                'multimedia_file_refn' => mb_substr($file, 0, 248),
800
                'multimedia_format'    => mb_substr($format, 0, 4),
801
                'source_media_type'    => mb_substr($type, 0, 15),
802
                'descriptive_title'    => mb_substr($title, 0, 248),
803
            ]);
804
        }
805
806
        return $xref;
807
    }
808
809
    /**
810
     * update a record in the database
811
     *
812
     * @param string $gedrec
813
     * @param Tree   $tree
814
     * @param bool   $delete
815
     *
816
     * @return void
817
     * @throws GedcomErrorException
818
     */
819
    public static function updateRecord(string $gedrec, Tree $tree, bool $delete): void
820
    {
821
        if (preg_match('/^0 @(' . Gedcom::REGEX_XREF . ')@ (' . Gedcom::REGEX_TAG . ')/', $gedrec, $match)) {
822
            [, $gid, $type] = $match;
823
        } elseif (preg_match('/^0 (HEAD)(?:\n|$)/', $gedrec, $match)) {
824
            // The HEAD record has no XREF.  Any others?
825
            $gid  = $match[1];
826
            $type = $match[1];
827
        } else {
828
            throw new GedcomErrorException($gedrec);
829
        }
830
831
        // Place links
832
        DB::table('placelinks')
833
            ->where('pl_gid', '=', $gid)
834
            ->where('pl_file', '=', $tree->id())
835
            ->delete();
836
837
        // Orphaned places.  If we're deleting  "Westminster, London, England",
838
        // then we may also need to delete "London, England" and "England".
839
        do {
840
            $affected = DB::table('places')
841
                ->leftJoin('placelinks', static function (JoinClause $join): void {
842
                    $join
843
                        ->on('p_id', '=', 'pl_p_id')
844
                        ->on('p_file', '=', 'pl_file');
845
                })
846
                ->whereNull('pl_p_id')
847
                ->delete();
848
        } while ($affected > 0);
849
850
        DB::table('dates')
851
            ->where('d_gid', '=', $gid)
852
            ->where('d_file', '=', $tree->id())
853
            ->delete();
854
855
        DB::table('name')
856
            ->where('n_id', '=', $gid)
857
            ->where('n_file', '=', $tree->id())
858
            ->delete();
859
860
        DB::table('link')
861
            ->where('l_from', '=', $gid)
862
            ->where('l_file', '=', $tree->id())
863
            ->delete();
864
865
        switch ($type) {
866
            case Individual::RECORD_TYPE:
867
                DB::table('individuals')
868
                    ->where('i_id', '=', $gid)
869
                    ->where('i_file', '=', $tree->id())
870
                    ->delete();
871
                break;
872
873
            case Family::RECORD_TYPE:
874
                DB::table('families')
875
                    ->where('f_id', '=', $gid)
876
                    ->where('f_file', '=', $tree->id())
877
                    ->delete();
878
                break;
879
880
            case Source::RECORD_TYPE:
881
                DB::table('sources')
882
                    ->where('s_id', '=', $gid)
883
                    ->where('s_file', '=', $tree->id())
884
                    ->delete();
885
                break;
886
887
            case Media::RECORD_TYPE:
888
                DB::table('media_file')
889
                    ->where('m_id', '=', $gid)
890
                    ->where('m_file', '=', $tree->id())
891
                    ->delete();
892
893
                DB::table('media')
894
                    ->where('m_id', '=', $gid)
895
                    ->where('m_file', '=', $tree->id())
896
                    ->delete();
897
                break;
898
899
            default:
900
                DB::table('other')
901
                    ->where('o_id', '=', $gid)
902
                    ->where('o_file', '=', $tree->id())
903
                    ->delete();
904
                break;
905
        }
906
907
        if (!$delete) {
908
            self::importRecord($gedrec, $tree, true);
909
        }
910
    }
911
}
912