StatisticsData   F
last analyzed

Complexity

Total Complexity 267

Size/Duplication

Total Lines 2977
Duplicated Lines 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 1743
dl 0
loc 2977
rs 0.8
c 1
b 0
f 0
wmc 267

100 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 4 1
A averageChildrenPerFamily() 0 5 1
F ageOfMarriageQuery() 0 153 23
A countEventQuery() 0 14 3
A countFirstChildrenByMonth() 0 7 1
A countIndividualsDeceased() 0 10 2
A topTenLargestGrandFamilyList() 0 4 1
A countHits() 0 7 1
A usersLoggedInList() 0 3 1
A countMarriedFemales() 0 7 1
A longlifeQuery() 0 12 1
A firstEventType() 0 17 4
B chartDistribution() 0 42 6
A countAllPlaces() 0 5 1
A topAgeBetweenSiblings() 0 9 2
A maximumAgeBetweenSiblings() 0 40 1
A countIndividualsWithSources() 0 12 1
A familiesWithTheMostChildren() 0 12 1
A topTenOldestAliveQuery() 0 27 5
A ageBetweenSpousesMFList() 0 6 1
A countTreeNews() 0 5 1
A countIndividualEventsByCountry() 0 18 1
A countEventsByMonth() 0 7 1
B getAllCountries() 0 513 1
A countIndividualsLiving() 0 10 2
A countMediaByType() 0 15 1
B iso3166() 0 247 1
A countPlacesForIndividuals() 0 9 1
A countFirstChildrenQuery() 0 34 3
A getIso3166Countries() 0 21 3
A firstEvent() 0 26 3
A countEventsByCentury() 0 9 1
A isUserLoggedIn() 0 5 2
A commonGivenNames() 0 41 5
A birthAndDeathQuery() 0 24 2
A firstEventYear() 0 15 3
A topTenLargestGrandFamily() 0 4 1
A firstEventName() 0 13 3
A countUserJournal() 0 5 1
A topAgeBetweenSiblingsList() 0 21 4
A usersLoggedIn() 0 3 1
A countFamilies() 0 5 1
A statsMarrAgeQuery() 0 34 4
A countPlaces() 0 23 4
A topTenFamilyQuery() 0 14 1
A countTreeFavorites() 0 5 1
A countRepositories() 0 6 1
F usersLoggedInQuery() 0 71 20
A countEventsByMonthAndSex() 0 17 1
A topTenLargestFamilyList() 0 6 1
A statsAgeQuery() 0 17 3
A countSurnames() 0 15 2
A commonSurnames() 0 44 5
B marriageQuery() 0 74 10
A ageBetweenSpousesFM() 0 4 1
A calculateAge() 0 15 3
A countSurnamesByCountry() 0 34 3
B parentsQuery() 0 74 10
A averageLifespanDays() 0 5 1
A topTenGrandFamilyQuery() 0 41 3
A ageBetweenSpousesFMList() 0 4 1
A commonSurnamesQuery() 0 23 2
A firstEventRecord() 0 16 4
A countFamiliesWithSources() 0 12 1
A countFamiliesWithEvents() 0 11 1
A countSources() 0 5 1
A statsChildrenQuery() 0 25 3
A topTenLargestFamily() 0 6 1
A filterEventPlaces() 0 19 4
A countUserMessages() 0 5 1
A mapTwoLetterToName() 0 6 2
A countIndividualsBySex() 0 6 1
A countFirstChildrenByMonthAndSex() 0 17 1
A countFamilyEventsByCountry() 0 18 1
A countIndividualsByCountry() 0 33 3
A topAgeBetweenSiblingsFullName() 0 9 2
A countChildren() 0 5 1
A countMarriedMales() 0 7 1
A countUserfavorites() 0 5 1
A countNotes() 0 6 1
A countIndividualsWithEvents() 0 11 1
A countPlacesForFamilies() 0 9 1
A createChartData() 0 21 2
A latestUserId() 0 14 2
A countFamiliesWithNoChildren() 0 6 1
A countGivenNames() 0 17 2
A topTenOldestQuery() 0 11 1
A ageBetweenSpousesMF() 0 6 1
A ageBetweenSpousesQuery() 0 52 3
A countOtherEvents() 0 6 1
D centuryName() 0 52 23
A countMedia() 0 10 2
A firstEventPlace() 0 18 4
B countFirstMarriagesByMonth() 0 47 6
A countIndividuals() 0 5 1
A countAllRecords() 0 9 1
A noChildrenFamiliesList() 0 30 5
A countAllEvents() 0 6 1
A countCountries() 0 21 1
A statsAge() 0 35 1

How to fix   Complexity   

Complex Class

Complex classes like StatisticsData 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 StatisticsData, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
/**
4
 * webtrees: online genealogy
5
 * Copyright (C) 2025 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;
21
22
use Fisharebest\Webtrees\Contracts\UserInterface;
23
use Fisharebest\Webtrees\Elements\UnknownElement;
24
use Fisharebest\Webtrees\Http\RequestHandlers\MessagePage;
25
use Fisharebest\Webtrees\Module\IndividualListModule;
26
use Fisharebest\Webtrees\Module\ModuleInterface;
27
use Fisharebest\Webtrees\Module\ModuleListInterface;
28
use Fisharebest\Webtrees\Services\MessageService;
29
use Fisharebest\Webtrees\Services\ModuleService;
30
use Fisharebest\Webtrees\Services\UserService;
31
use Illuminate\Database\Query\Builder;
32
use Illuminate\Database\Query\Expression;
33
use Illuminate\Database\Query\JoinClause;
34
use Illuminate\Support\Collection;
35
36
use function abs;
37
use function app;
38
use function array_keys;
39
use function array_reverse;
40
use function array_search;
41
use function array_shift;
42
use function array_slice;
43
use function arsort;
44
use function asort;
45
use function count;
46
use function e;
47
use function explode;
48
use function floor;
49
use function implode;
50
use function in_array;
51
use function preg_match;
52
use function preg_quote;
53
use function route;
54
use function str_replace;
55
use function strip_tags;
56
use function uksort;
57
use function view;
0 ignored issues
show
Bug introduced by
This use statement conflicts with another class in this namespace, Fisharebest\Webtrees\view. Consider defining an alias.

Let?s assume that you have a directory layout like this:

.
|-- OtherDir
|   |-- Bar.php
|   `-- Foo.php
`-- SomeDir
    `-- Foo.php

and let?s assume the following content of Bar.php:

// Bar.php
namespace OtherDir;

use SomeDir\Foo; // This now conflicts the class OtherDir\Foo

If both files OtherDir/Foo.php and SomeDir/Foo.php are loaded in the same runtime, you will see a PHP error such as the following:

PHP Fatal error:  Cannot use SomeDir\Foo as Foo because the name is already in use in OtherDir/Foo.php

However, as OtherDir/Foo.php does not necessarily have to be loaded and the error is only triggered if it is loaded before OtherDir/Bar.php, this problem might go unnoticed for a while. In order to prevent this error from surfacing, you must import the namespace with a different alias:

// Bar.php
namespace OtherDir;

use SomeDir\Foo as SomeDirFoo; // There is no conflict anymore.
Loading history...
58
59
class StatisticsData
60
{
61
    private Tree $tree;
62
    private UserService $user_service;
63
64
    public function __construct(Tree $tree, UserService $user_service)
65
    {
66
        $this->tree         = $tree;
67
        $this->user_service = $user_service;
68
    }
69
70
    public function averageChildrenPerFamily(): float
71
    {
72
        return (float) DB::table('families')
73
            ->where('f_file', '=', $this->tree->id())
74
            ->avg('f_numchil');
75
    }
76
77
    public function averageLifespanDays(string $sex): int
78
    {
79
        return (int) $this->birthAndDeathQuery($sex)
80
            ->select([new Expression('AVG(' . DB::prefix('death.d_julianday2') . ' - ' . DB::prefix('birth.d_julianday1') . ') AS days')])
81
            ->value('days');
82
    }
83
84
    /**
85
     * @return Collection<string,int>
86
     */
87
    public function commonGivenNames(string $sex, int $threshold, int $limit): Collection
88
    {
89
        $query = DB::table('name')
90
            ->where('n_file', '=', $this->tree->id())
91
            ->where('n_type', '<>', '_MARNM')
92
            ->where('n_givn', '<>', Individual::PRAENOMEN_NESCIO)
93
            ->where(new Expression('LENGTH(n_givn)'), '>', 1);
94
95
        if ($sex !== 'ALL') {
96
            $query
97
                ->join('individuals', static function (JoinClause $join): void {
98
                    $join
99
                        ->on('i_file', '=', 'n_file')
100
                        ->on('i_id', '=', 'n_id');
101
                })
102
                ->where('i_sex', '=', $sex);
103
        }
104
105
        $rows = $query
106
            ->groupBy(['n_givn'])
107
            ->pluck(new Expression('COUNT(DISTINCT n_id)'), 'n_givn')
108
            ->map(static fn ($count): int => (int) $count);
109
110
111
        $given_names = [];
112
113
        foreach ($rows as $n_givn => $count) {
114
            // Split “John Thomas” into “John” and “Thomas” and count against both totals
115
            foreach (explode(' ', (string) $n_givn) as $given) {
116
                // Exclude initials and particles.
117
                if (preg_match('/^([A-Z]|[a-z]{1,3})$/', $given) !== 1) {
118
                    $given_names[$given] ??= 0;
119
                    $given_names[$given] += (int) $count;
120
                }
121
            }
122
        }
123
124
        return (new Collection($given_names))
125
            ->sortDesc()
126
            ->slice(0, $limit)
127
            ->filter(static fn (int $count): bool => $count >= $threshold);
128
    }
129
130
    /**
131
     * @return array<array<int>>
132
     */
133
    public function commonSurnames(int $limit, int $threshold, string $sort): array
134
    {
135
        // Use the count of base surnames.
136
        $top_surnames = DB::table('name')
137
            ->where('n_file', '=', $this->tree->id())
138
            ->where('n_type', '<>', '_MARNM')
139
            ->whereNotIn('n_surn', ['', Individual::NOMEN_NESCIO])
140
            ->select(['n_surn'])
141
            ->groupBy(['n_surn'])
142
            ->orderByRaw('COUNT(n_surn) DESC')
143
            ->orderBy(new Expression('COUNT(n_surn)'), 'DESC')
144
            ->having(new Expression('COUNT(n_surn)'), '>=', $threshold)
145
            ->take($limit)
146
            ->pluck('n_surn')
147
            ->all();
148
149
        $surnames = [];
150
151
        foreach ($top_surnames as $top_surname) {
152
            $surnames[$top_surname] = DB::table('name')
153
                ->where('n_file', '=', $this->tree->id())
154
                ->where('n_type', '<>', '_MARNM')
155
                ->where('n_surn', '=', $top_surname)
156
                ->select(['n_surn', new Expression('COUNT(n_surn) AS count')])
157
                ->groupBy(['n_surn'])
158
                ->orderBy('n_surn')
159
                ->pluck('count', 'n_surn')
160
                ->map(static fn (string $count): int => (int) $count)
161
                ->all();
162
        }
163
164
        switch ($sort) {
165
            default:
166
            case 'alpha':
167
                uksort($surnames, I18N::comparator());
168
                break;
169
            case 'count':
170
                break;
171
            case 'rcount':
172
                $surnames = array_reverse($surnames, true);
173
                break;
174
        }
175
176
        return $surnames;
177
    }
178
179
    /**
180
     * @param array<string> $events
181
     */
182
    public function countAllEvents(array $events): int
183
    {
184
        return DB::table('dates')
185
            ->where('d_file', '=', $this->tree->id())
186
            ->whereIn('d_fact', $events)
187
            ->count();
188
    }
189
190
    public function countAllPlaces(): int
191
    {
192
        return DB::table('places')
193
            ->where('p_file', '=', $this->tree->id())
194
            ->count();
195
    }
196
197
    public function countAllRecords(): int
198
    {
199
        return
200
            $this->countIndividuals() +
201
            $this->countFamilies() +
202
            $this->countMedia() +
203
            $this->countNotes() +
204
            $this->countRepositories() +
205
            $this->countSources();
206
    }
207
208
    public function countChildren(): int
209
    {
210
        return (int) DB::table('families')
211
            ->where('f_file', '=', $this->tree->id())
212
            ->sum('f_numchil');
213
    }
214
215
    /**
216
     * @return array<array{place:Place,count:int}>
217
     */
218
    public function countCountries(int $limit): array
219
    {
220
        return DB::table('places')
221
            ->join('placelinks', static function (JoinClause $join): void {
222
                $join
223
                    ->on('pl_file', '=', 'p_file')
224
                    ->on('pl_p_id', '=', 'p_id');
225
            })
226
            ->where('p_file', '=', $this->tree->id())
227
            ->where('p_parent_id', '=', 0)
228
            ->groupBy(['p_place'])
229
            ->orderByDesc(new Expression('COUNT(*)'))
230
            ->orderBy('p_place')
231
            ->take($limit)
232
            ->select([new Expression('COUNT(*) AS total'), 'p_place AS place'])
233
            ->get()
234
            ->map(fn (object $row): array => [
235
                'place' => new Place($row->place, $this->tree),
236
                'count' => (int) $row->total,
237
            ])
238
            ->all();
239
    }
240
241
    private function countEventQuery(string $event, int $year1 = 0, int $year2 = 0): Builder
242
    {
243
        $query = DB::table('dates')
244
            ->where('d_file', '=', $this->tree->id())
245
            ->where('d_fact', '=', $event)
246
            ->whereIn('d_type', ['@#DGREGORIAN@', '@#DJULIAN@']);
247
248
        if ($year1 !== 0 && $year2 !== 0) {
249
            $query->whereBetween('d_year', [$year1, $year2]);
250
        } else {
251
            $query->where('d_year', '<>', 0);
252
        }
253
254
        return $query;
255
    }
256
257
    /**
258
     * @return array<string,int>
259
     */
260
    public function countEventsByMonth(string $event, int $year1, int $year2): array
261
    {
262
        return $this->countEventQuery($event, $year1, $year2)
263
            ->groupBy(['d_month'])
264
            ->pluck(new Expression('COUNT(*)'), 'd_month')
265
            ->map(static fn (string $total): int => (int) $total)
266
            ->all();
267
    }
268
269
    /**
270
     * @return array<object{month:string,sex:string,total:int}>
0 ignored issues
show
Documentation Bug introduced by
The doc comment array<object{month:string,sex:string,total:int}> at position 2 could not be parsed: Expected '>' at position 2, but found 'object'.
Loading history...
271
     */
272
    public function countEventsByMonthAndSex(string $event, int $year1, int $year2): array
273
    {
274
        return $this->countEventQuery($event, $year1, $year2)
275
            ->join('individuals', static function (JoinClause $join): void {
276
                $join
277
                    ->on('i_id', '=', 'd_gid')
278
                    ->on('i_file', '=', 'd_file');
279
            })
280
            ->groupBy(['i_sex', 'd_month'])
281
            ->select(['d_month', 'i_sex', new Expression('COUNT(*) AS total')])
282
            ->get()
283
            ->map(static fn (object $row): object => (object) [
284
                'month' => $row->d_month,
285
                'sex'   => $row->i_sex,
286
                'total' => (int) $row->total,
287
            ])
288
            ->all();
289
    }
290
291
    /**
292
     * @return array<int,array{0:string,1:int}>
293
     */
294
    public function countEventsByCentury(string $event): array
295
    {
296
        return $this->countEventQuery($event, 0, 0)
297
            ->select([new Expression('ROUND((d_year + 49) / 100, 0) AS century'), new Expression('COUNT(*) AS total')])
298
            ->groupBy(['century'])
299
            ->orderBy('century')
300
            ->get()
301
            ->map(fn (object $row): array => [$this->centuryName((int) $row->century), (int) $row->total])
302
            ->all();
303
    }
304
305
    public function countFamilies(): int
306
    {
307
        return DB::table('families')
308
            ->where('f_file', '=', $this->tree->id())
309
            ->count();
310
    }
311
312
    /**
313
     * @param array<string> $events
314
     */
315
    public function countFamiliesWithEvents(array $events): int
316
    {
317
        return DB::table('dates')
318
            ->join('families', static function (JoinClause $join): void {
319
                $join
320
                    ->on('f_id', '=', 'd_gid')
321
                    ->on('f_file', '=', 'd_file');
322
            })
323
            ->where('d_file', '=', $this->tree->id())
324
            ->whereIn('d_fact', $events)
325
            ->count();
326
    }
327
328
    public function countFamiliesWithNoChildren(): int
329
    {
330
        return DB::table('families')
331
            ->where('f_file', '=', $this->tree->id())
332
            ->where('f_numchil', '=', 0)
333
            ->count();
334
    }
335
336
    public function countFamiliesWithSources(): int
337
    {
338
        return DB::table('families')
339
            ->select(['f_id'])
340
            ->distinct()
341
            ->join('link', static function (JoinClause $join): void {
342
                $join->on('f_id', '=', 'l_from')
343
                    ->on('f_file', '=', 'l_file');
344
            })
345
            ->where('l_file', '=', $this->tree->id())
346
            ->where('l_type', '=', 'SOUR')
347
            ->count('f_id');
348
    }
349
350
    /**
351
     * @return array<string,int>
352
     */
353
    public function countFirstChildrenByMonth(int $year1, int $year2): array
354
    {
355
        return $this->countFirstChildrenQuery($year1, $year2)
356
            ->groupBy(['d_month'])
357
            ->pluck(new Expression('COUNT(*)'), 'd_month')
358
            ->map(static fn (string $total): int => (int) $total)
359
            ->all();
360
    }
361
362
    /**
363
     * @return array<object{month:string,sex:string,total:int}>
0 ignored issues
show
Documentation Bug introduced by
The doc comment array<object{month:string,sex:string,total:int}> at position 2 could not be parsed: Expected '>' at position 2, but found 'object'.
Loading history...
364
     */
365
    public function countFirstChildrenByMonthAndSex(int $year1, int $year2): array
366
    {
367
        return $this->countFirstChildrenQuery($year1, $year2)
368
            ->join('individuals', static function (JoinClause $join): void {
369
                $join
370
                    ->on('i_file', '=', 'l_file')
371
                    ->on('i_id', '=', 'l_to');
372
            })
373
            ->groupBy(['d_month', 'i_sex'])
374
            ->select(['d_month', 'i_sex', new Expression('COUNT(*) AS total')])
375
            ->get()
376
            ->map(static fn (object $row): object => (object) [
377
                'month' => $row->d_month,
378
                'sex'   => $row->i_sex,
379
                'total' => (int) $row->total,
380
            ])
381
            ->all();
382
    }
383
384
    private function countFirstChildrenQuery(int $year1, int $year2): Builder
385
    {
386
        $first_child_subquery = DB::table('link')
387
            ->join('dates', static function (JoinClause $join): void {
388
                $join
389
                    ->on('d_gid', '=', 'l_to')
390
                    ->on('d_file', '=', 'l_file')
391
                    ->where('d_julianday1', '<>', 0)
392
                    ->whereIn('d_month', ['JAN', 'FEB', 'MAR', 'APR', 'MAY', 'JUN', 'JUL', 'AUG', 'SEP', 'OCT', 'NOV', 'DEC']);
393
            })
394
            ->where('l_file', '=', $this->tree->id())
395
            ->where('l_type', '=', 'CHIL')
396
            ->select(['l_from AS family_id', new Expression('MIN(d_julianday1) AS min_birth_jd')])
397
            ->groupBy(['family_id']);
398
399
        $query = DB::table('link')
400
            ->join('dates', static function (JoinClause $join): void {
401
                $join
402
                    ->on('d_gid', '=', 'l_to')
403
                    ->on('d_file', '=', 'l_file');
404
            })
405
            ->joinSub($first_child_subquery, 'subquery', static function (JoinClause $join): void {
406
                $join
407
                    ->on('family_id', '=', 'l_from')
408
                    ->on('min_birth_jd', '=', 'd_julianday1');
409
            })
410
            ->where('link.l_file', '=', $this->tree->id())
411
            ->where('link.l_type', '=', 'CHIL');
412
413
        if ($year1 !== 0 && $year2 !== 0) {
414
            $query->whereBetween('d_year', [$year1, $year2]);
415
        }
416
417
        return $query;
418
    }
419
420
    /**
421
     * @return array<string,int>
422
     */
423
    public function countFirstMarriagesByMonth(Tree $tree, int $year1, int $year2): array
424
    {
425
        $query = DB::table('families')
426
            ->join('dates', static function (JoinClause $join): void {
427
                $join
428
                    ->on('d_gid', '=', 'f_id')
429
                    ->on('d_file', '=', 'f_file')
430
                    ->where('d_fact', '=', 'MARR')
431
                    ->whereIn('d_month', ['JAN', 'FEB', 'MAR', 'APR', 'MAY', 'JUN', 'JUL', 'AUG', 'SEP', 'OCT', 'NOV', 'DEC'])
432
                    ->where('d_julianday2', '<>', 0);
433
            })
434
            ->where('f_file', '=', $tree->id());
435
436
        if ($year1 !== 0 && $year2 !== 0) {
437
            $query->whereBetween('d_year', [$year1, $year2]);
438
        }
439
440
        $rows = $query
441
            ->orderBy('d_julianday2')
442
            ->select(['f_husb AS husb', 'f_wife AS wife', 'd_month AS month'])
443
            ->get();
444
445
        $months = [
446
            'JAN' => 0,
447
            'FEB' => 0,
448
            'MAR' => 0,
449
            'APR' => 0,
450
            'MAY' => 0,
451
            'JUN' => 0,
452
            'JUL' => 0,
453
            'AUG' => 0,
454
            'SEP' => 0,
455
            'OCT' => 0,
456
            'NOV' => 0,
457
            'DEC' => 0,
458
        ];
459
        $seen = [];
460
461
        foreach ($rows as $row) {
462
            if (!in_array($row->husb, $seen, true) && !in_array($row->wife, $seen, true)) {
463
                $months[$row->month]++;
464
                $seen[] = $row->husb;
465
                $seen[] = $row->wife;
466
            }
467
        }
468
469
        return $months;
470
    }
471
472
    /**
473
     * @param array<string> $names
474
     */
475
    public function countGivenNames(array $names): int
476
    {
477
        if ($names === []) {
478
            // Count number of distinct given names.
479
            return DB::table('name')
480
                ->where('n_file', '=', $this->tree->id())
481
                ->distinct()
482
                ->where('n_givn', '<>', Individual::PRAENOMEN_NESCIO)
483
                ->whereNotNull('n_givn')
484
                ->count('n_givn');
485
        }
486
487
        // Count number of occurrences of specific given names.
488
        return DB::table('name')
489
            ->where('n_file', '=', $this->tree->id())
490
            ->whereIn('n_givn', $names)
491
            ->count('n_givn');
492
    }
493
494
    public function countHits(string $page_name, string $page_parameter): int
495
    {
496
        return (int) DB::table('hit_counter')
497
            ->where('gedcom_id', '=', $this->tree->id())
498
            ->where('page_name', '=', $page_name)
499
            ->where('page_parameter', '=', $page_parameter)
500
            ->sum('page_count');
501
    }
502
503
    public function countIndividuals(): int
504
    {
505
        return DB::table('individuals')
506
            ->where('i_file', '=', $this->tree->id())
507
            ->count();
508
    }
509
510
    public function countIndividualsBySex(string $sex): int
511
    {
512
        return DB::table('individuals')
513
            ->where('i_file', '=', $this->tree->id())
514
            ->where('i_sex', '=', $sex)
515
            ->count();
516
    }
517
518
    public function countIndividualsDeceased(): int
519
    {
520
        return DB::table('individuals')
521
            ->where('i_file', '=', $this->tree->id())
522
            ->where(static function (Builder $query): void {
523
                foreach (Gedcom::DEATH_EVENTS as $death_event) {
524
                    $query->orWhere('i_gedcom', 'LIKE', "%\n1 " . $death_event . '%');
525
                }
526
            })
527
            ->count();
528
    }
529
530
    public function countIndividualsLiving(): int
531
    {
532
        $query = DB::table('individuals')
533
            ->where('i_file', '=', $this->tree->id());
534
535
        foreach (Gedcom::DEATH_EVENTS as $death_event) {
536
            $query->where('i_gedcom', 'NOT LIKE', "%\n1 " . $death_event . '%');
537
        }
538
539
        return $query->count();
540
    }
541
542
    /**
543
     * @param array<string> $events
544
     */
545
    public function countIndividualsWithEvents(array $events): int
546
    {
547
        return DB::table('dates')
548
            ->join('individuals', static function (JoinClause $join): void {
549
                $join
550
                    ->on('i_id', '=', 'd_gid')
551
                    ->on('i_file', '=', 'd_file');
552
            })
553
            ->where('d_file', '=', $this->tree->id())
554
            ->whereIn('d_fact', $events)
555
            ->count();
556
    }
557
558
    public function countIndividualsWithSources(): int
559
    {
560
        return DB::table('individuals')
561
            ->select(['i_id'])
562
            ->distinct()
563
            ->join('link', static function (JoinClause $join): void {
564
                $join->on('i_id', '=', 'l_from')
565
                    ->on('i_file', '=', 'l_file');
566
            })
567
            ->where('l_file', '=', $this->tree->id())
568
            ->where('l_type', '=', 'SOUR')
569
            ->count('i_id');
570
    }
571
572
    public function countMarriedFemales(): int
573
    {
574
        return DB::table('families')
575
            ->where('f_file', '=', $this->tree->id())
576
            ->where('f_gedcom', 'LIKE', "%\n1 MARR%")
577
            ->distinct()
578
            ->count('f_wife');
579
    }
580
581
    public function countMarriedMales(): int
582
    {
583
        return DB::table('families')
584
            ->where('f_file', '=', $this->tree->id())
585
            ->where('f_gedcom', 'LIKE', "%\n1 MARR%")
586
            ->distinct()
587
            ->count('f_husb');
588
    }
589
590
    public function countMedia(string $type = 'all'): int
591
    {
592
        $query = DB::table('media_file')
593
            ->where('m_file', '=', $this->tree->id());
594
595
        if ($type !== 'all') {
596
            $query->where('source_media_type', '=', $type);
597
        }
598
599
        return $query->count();
600
    }
601
602
    /**
603
     * @return array<array{0:string,1:int}>
604
     */
605
    public function countMediaByType(): array
606
    {
607
        $element = Registry::elementFactory()->make('OBJE:FILE:FORM:TYPE');
608
        $values  = $element->values();
609
610
        return DB::table('media_file')
611
            ->where('m_file', '=', $this->tree->id())
612
            ->groupBy('source_media_type')
613
            ->select([new Expression('COUNT(*) AS total'), 'source_media_type'])
614
            ->get()
615
            ->map(static fn (object $row): array => [
616
                $values[$element->canonical($row->source_media_type)] ?? I18N::translate('Other'),
617
                (int) $row->total,
618
            ])
619
            ->all();
620
    }
621
622
    public function countNotes(): int
623
    {
624
        return DB::table('other')
625
            ->where('o_file', '=', $this->tree->id())
626
            ->where('o_type', '=', 'NOTE')
627
            ->count();
628
    }
629
630
    /**
631
     * @param array<string> $events
632
     */
633
    public function countOtherEvents(array $events): int
634
    {
635
        return DB::table('dates')
636
            ->where('d_file', '=', $this->tree->id())
637
            ->whereNotIn('d_fact', $events)
638
            ->count();
639
    }
640
641
    /**
642
     * @param array<string> $rows
643
     *
644
     * @return array<array{place:Place,count:int}>
645
     */
646
    private function countPlaces(array $rows, string $event, int $limit): array
647
    {
648
        $places = [];
649
650
        foreach ($rows as $gedcom) {
651
            if (preg_match('/\n1 ' . $event . '(?:\n[2-9].*)*\n2 PLAC (.+)/', $gedcom, $match) === 1) {
652
                $places[$match[1]] ??= 0;
653
                $places[$match[1]]++;
654
            }
655
        }
656
657
        arsort($places);
658
659
        $records = [];
660
661
        foreach (array_slice($places, 0, $limit) as $place => $count) {
662
            $records[] = [
663
                'place' => new Place((string) $place, $this->tree),
664
                'count' => $count,
665
            ];
666
        }
667
668
        return $records;
669
    }
670
671
    /**
672
     * @return array<array{place:Place,count:int}>
673
     */
674
    public function countPlacesForFamilies(string $event, int $limit): array
675
    {
676
        $rows = DB::table('families')
677
            ->where('f_file', '=', $this->tree->id())
678
            ->where('f_gedcom', 'LIKE', "%\n2 PLAC %")
679
            ->pluck('f_gedcom')
680
            ->all();
681
682
        return $this->countPlaces($rows, $event, $limit);
683
    }
684
685
    /**
686
     * @return array<array{place:Place,count:int}>
687
     */
688
    public function countPlacesForIndividuals(string $event, int $limit): array
689
    {
690
        $rows = DB::table('individuals')
691
            ->where('i_file', '=', $this->tree->id())
692
            ->where('i_gedcom', 'LIKE', "%\n2 PLAC %")
693
            ->pluck('i_gedcom')
694
            ->all();
695
696
        return $this->countPlaces($rows, $event, $limit);
697
    }
698
699
    public function countRepositories(): int
700
    {
701
        return DB::table('other')
702
            ->where('o_file', '=', $this->tree->id())
703
            ->where('o_type', '=', 'REPO')
704
            ->count();
705
    }
706
707
    public function countSources(): int
708
    {
709
        return DB::table('sources')
710
            ->where('s_file', '=', $this->tree->id())
711
            ->count();
712
    }
713
714
    /**
715
     * @param array<string> $names
716
     */
717
    public function countSurnames(array $names): int
718
    {
719
        if ($names === []) {
720
            // Count number of distinct surnames
721
            return DB::table('name')
722
                ->where('n_file', '=', $this->tree->id())->distinct()
723
                ->whereNotNull('n_surn')
724
                ->count('n_surn');
725
        }
726
727
        // Count number of occurrences of specific surnames.
728
        return DB::table('name')
729
            ->where('n_file', '=', $this->tree->id())
730
            ->whereIn('n_surn', $names)
731
            ->count('n_surn');
732
    }
733
734
    public function countTreeFavorites(): int
735
    {
736
        return DB::table('favorite')
737
            ->where('gedcom_id', '=', $this->tree->id())
738
            ->count();
739
    }
740
741
    public function countTreeNews(): int
742
    {
743
        return DB::table('news')
744
            ->where('gedcom_id', '=', $this->tree->id())
745
            ->count();
746
    }
747
748
    public function countUserfavorites(): int
749
    {
750
        return DB::table('favorite')
751
            ->where('user_id', '=', Auth::id())
752
            ->count();
753
    }
754
755
    public function countUserJournal(): int
756
    {
757
        return DB::table('news')
758
            ->where('user_id', '=', Auth::id())
759
            ->count();
760
    }
761
762
    public function countUserMessages(): int
763
    {
764
        return DB::table('message')
765
            ->where('user_id', '=', Auth::id())
766
            ->count();
767
    }
768
769
    /**
770
     * @return array<int,object{family:Family,children:int}>
0 ignored issues
show
Documentation Bug introduced by
The doc comment array<int,object{family:Family,children:int}> at position 4 could not be parsed: Expected '>' at position 4, but found 'object'.
Loading history...
771
     */
772
    public function familiesWithTheMostChildren(int $limit): array
773
    {
774
        return DB::table('families')
775
            ->where('f_file', '=', $this->tree->id())
776
            ->orderByDesc('f_numchil')
777
            ->limit($limit)
778
            ->get()
779
            ->map(fn (object $row): object => (object) [
780
                'family'   => Registry::familyFactory()->make($row->f_id, $this->tree, $row->f_gedcom),
781
                'children' => (int) $row->f_numchil,
782
            ])
783
            ->all();
784
    }
785
786
    /**
787
     * @param array<string> $events
788
     *
789
     * @return object{id:string,year:int,fact:string,type:string}|null
790
     */
791
    private function firstEvent(array $events, bool $ascending): ?object
792
    {
793
        if ($events === []) {
794
            $events = [
795
                ...Gedcom::BIRTH_EVENTS,
796
                ...Gedcom::DEATH_EVENTS,
797
                ...Gedcom::MARRIAGE_EVENTS,
798
                ...Gedcom::DIVORCE_EVENTS,
799
            ];
800
        }
801
802
        return DB::table('dates')
803
            ->select(['d_gid as id', 'd_year as year', 'd_fact AS fact', 'd_type AS type'])
804
            ->where('d_file', '=', $this->tree->id())
805
            ->whereIn('d_fact', $events)
806
            ->where('d_julianday1', '<>', 0)
807
            ->orderBy('d_julianday1', $ascending ? 'ASC' : 'DESC')
808
            ->limit(1)
809
            ->get()
810
            ->map(static fn (object $row): object => (object) [
811
                'id'   => $row->id,
812
                'year' => (int) $row->year,
813
                'fact' => $row->fact,
814
                'type' => $row->type,
815
            ])
816
            ->first();
817
    }
818
819
    /**
820
     * @param array<string> $events
821
     */
822
    public function firstEventName(array $events, bool $ascending): string
823
    {
824
        $row = $this->firstEvent($events, $ascending);
825
826
        if ($row !== null) {
827
            $record = Registry::gedcomRecordFactory()->make($row->id, $this->tree);
828
829
            if ($record instanceof GedcomRecord) {
830
                return '<a href="' . e($record->url()) . '">' . $record->fullName() . '</a>';
831
            }
832
        }
833
834
        return '';
835
    }
836
837
    /**
838
     * @param array<string> $events
839
     */
840
    public function firstEventPlace(array $events, bool $ascending): string
841
    {
842
        $row = $this->firstEvent($events, $ascending);
843
844
        if ($row !== null) {
845
            $record = Registry::gedcomRecordFactory()->make($row->id, $this->tree);
846
            $fact   = null;
847
848
            if ($record instanceof GedcomRecord) {
849
                $fact = $record->facts([$row->fact])->first();
850
            }
851
852
            if ($fact instanceof Fact) {
853
                return $fact->place()->shortName();
854
            }
855
        }
856
857
        return I18N::translate('Private');
858
    }
859
860
    /**
861
     * @param array<string> $events
862
     */
863
    public function firstEventRecord(array $events, bool $ascending): string
864
    {
865
        $row = $this->firstEvent($events, $ascending);
866
        $result = I18N::translate('This information is not available.');
867
868
        if ($row !== null) {
869
            $record = Registry::gedcomRecordFactory()->make($row->id, $this->tree);
870
871
            if ($record instanceof GedcomRecord && $record->canShow()) {
872
                $result = $record->formatList();
873
            } else {
874
                $result = I18N::translate('This information is private and cannot be shown.');
875
            }
876
        }
877
878
        return $result;
879
    }
880
881
    /**
882
     * @param array<string> $events
883
     */
884
    public function firstEventType(array $events, bool $ascending): string
885
    {
886
        $row = $this->firstEvent($events, $ascending);
887
888
        if ($row === null) {
889
            return '';
890
        }
891
892
        foreach ([Individual::RECORD_TYPE, Family::RECORD_TYPE] as $record_type) {
893
            $element = Registry::elementFactory()->make($record_type . ':' . $row->fact);
894
895
            if (!$element instanceof UnknownElement) {
896
                return $element->label();
897
            }
898
        }
899
900
        return $row->fact;
901
    }
902
903
    /**
904
     * @param array<string> $events
905
     */
906
    public function firstEventYear(array $events, bool $ascending): string
907
    {
908
        $row = $this->firstEvent($events, $ascending);
909
910
        if ($row === null) {
911
            return '-';
912
        }
913
914
        if ($row->year < 0) {
915
            $date = new Date($row->type . ' ' . abs($row->year) . ' B.C.');
916
        } else {
917
            $date = new Date($row->type . ' ' . $row->year);
918
        }
919
920
        return $date->display();
921
    }
922
923
    public function isUserLoggedIn(?int $user_id): bool
924
    {
925
        return $user_id !== null && DB::table('session')
926
            ->where('user_id', '=', $user_id)
927
            ->exists();
928
    }
929
930
    public function latestUserId(): ?int
931
    {
932
        $user_id = DB::table('user')
933
            ->select(['user.user_id'])
934
            ->leftJoin('user_setting', 'user.user_id', '=', 'user_setting.user_id')
935
            ->where('setting_name', '=', UserInterface::PREF_TIMESTAMP_REGISTERED)
936
            ->orderByDesc('setting_value')
937
            ->value('user_id');
938
939
        if ($user_id === null) {
940
            return null;
941
        }
942
943
        return (int) $user_id;
944
    }
945
946
    /**
947
     * @return array<object{family:Family,child1:Individual,child2:Individual,age:string}>
0 ignored issues
show
Documentation Bug introduced by
The doc comment array<object{family:Fami...Individual,age:string}> at position 2 could not be parsed: Expected '>' at position 2, but found 'object'.
Loading history...
948
     */
949
    public function maximumAgeBetweenSiblings(int $limit): array
950
    {
951
        return DB::table('link AS link1')
952
            ->join('link AS link2', static function (JoinClause $join): void {
953
                $join
954
                    ->on('link2.l_from', '=', 'link1.l_from')
955
                    ->on('link2.l_type', '=', 'link1.l_type')
956
                    ->on('link2.l_file', '=', 'link1.l_file');
957
            })
958
            ->join('dates AS child1', static function (JoinClause $join): void {
959
                $join
960
                    ->on('child1.d_gid', '=', 'link1.l_to')
961
                    ->on('child1.d_file', '=', 'link1.l_file')
962
                    ->where('child1.d_fact', '=', 'BIRT')
963
                    ->where('child1.d_julianday1', '<>', 0);
964
            })
965
            ->join('dates AS child2', static function (JoinClause $join): void {
966
                $join
967
                    ->on('child2.d_gid', '=', 'link2.l_to')
968
                    ->on('child2.d_file', '=', 'link2.l_file')
969
                    ->where('child2.d_fact', '=', 'BIRT')
970
                    ->whereColumn('child2.d_julianday2', '>', 'child1.d_julianday1');
971
            })
972
            ->where('link1.l_type', '=', 'CHIL')
973
            ->where('link1.l_file', '=', $this->tree->id())
974
            ->distinct()
975
            ->select(['link1.l_from AS family', 'link1.l_to AS child1', 'link2.l_to AS child2', new Expression(DB::prefix('child2.d_julianday2') . ' - ' . DB::prefix('child1.d_julianday1') . ' AS age')])
976
            ->orderBy('age', 'DESC')
977
            ->take($limit)
978
            ->get()
979
            ->map(fn (object $row): object => (object) [
980
                'family' => Registry::familyFactory()->make($row->family, $this->tree),
981
                'child1' => Registry::individualFactory()->make($row->child1, $this->tree),
982
                'child2' => Registry::individualFactory()->make($row->child2, $this->tree),
983
                'age'    => $this->calculateAge((int) $row->age),
984
            ])
985
            ->filter(static fn (object $row): bool => $row->family !== null)
986
            ->filter(static fn (object $row): bool => $row->child1 !== null)
987
            ->filter(static fn (object $row): bool => $row->child2 !== null)
988
            ->all();
989
    }
990
991
    /**
992
     * @return Collection<int,Individual>
993
     */
994
    public function topTenOldestAliveQuery(string $sex, int $limit): Collection
995
    {
996
        $query = DB::table('dates')
997
            ->join('individuals', static function (JoinClause $join): void {
998
                $join
999
                    ->on('i_id', '=', 'd_gid')
1000
                    ->on('i_file', '=', 'd_file');
1001
            })
1002
            ->where('d_file', '=', $this->tree->id())
1003
            ->where('d_julianday1', '<>', 0)
1004
            ->where('d_fact', '=', 'BIRT')
1005
            ->where('i_gedcom', 'NOT LIKE', "%\n1 DEAT%")
1006
            ->where('i_gedcom', 'NOT LIKE', "%\n1 BURI%")
1007
            ->where('i_gedcom', 'NOT LIKE', "%\n1 CREM%");
1008
1009
        if ($sex === 'F' || $sex === 'M' || $sex === 'U' || $sex === 'X') {
1010
            $query->where('i_sex', '=', $sex);
1011
        }
1012
1013
        return $query
1014
            ->groupBy(['i_id', 'i_file'])
1015
            ->orderBy(new Expression('MIN(d_julianday1)'))
1016
            ->select(['individuals.*'])
1017
            ->take($limit)
1018
            ->get()
1019
            ->map(Registry::individualFactory()->mapper($this->tree))
1020
            ->filter(GedcomRecord::accessFilter());
1021
    }
1022
1023
    public function commonSurnamesQuery(string $type, bool $totals, int $threshold, int $limit, string $sort): string
1024
    {
1025
        $surnames = $this->commonSurnames($limit, $threshold, $sort);
1026
1027
        // find a module providing individual lists
1028
        $module = app(ModuleService::class)
1029
            ->findByComponent(ModuleListInterface::class, $this->tree, Auth::user())
1030
            ->first(static fn (ModuleInterface $module): bool => $module instanceof IndividualListModule);
1031
1032
        if ($type === 'list') {
1033
            return view('lists/surnames-bullet-list', [
1034
                'surnames' => $surnames,
1035
                'module'   => $module,
1036
                'totals'   => $totals,
1037
                'tree'     => $this->tree,
1038
            ]);
1039
        }
1040
1041
        return view('lists/surnames-compact-list', [
1042
            'surnames' => $surnames,
1043
            'module'   => $module,
1044
            'totals'   => $totals,
1045
            'tree'     => $this->tree,
1046
        ]);
1047
    }
1048
1049
    /**
1050
     * @return  array<object{age:float,century:int,sex:string}>
0 ignored issues
show
Documentation Bug introduced by
The doc comment array<object{age:float,century:int,sex:string}> at position 2 could not be parsed: Expected '>' at position 2, but found 'object'.
Loading history...
1051
     */
1052
    public function statsAge(): array
1053
    {
1054
        return DB::table('individuals')
1055
            ->select([
1056
                new Expression('AVG(' . DB::prefix('death.d_julianday2') . ' - ' . DB::prefix('birth.d_julianday1') . ') / 365.25 AS age'),
1057
                new Expression('ROUND((' . DB::prefix('death.d_year') . ' + 49) / 100, 0) AS century'),
1058
                'i_sex AS sex'
1059
            ])
1060
            ->join('dates AS birth', static function (JoinClause $join): void {
1061
                $join
1062
                    ->on('birth.d_file', '=', 'i_file')
1063
                    ->on('birth.d_gid', '=', 'i_id');
1064
            })
1065
            ->join('dates AS death', static function (JoinClause $join): void {
1066
                $join
1067
                    ->on('death.d_file', '=', 'i_file')
1068
                    ->on('death.d_gid', '=', 'i_id');
1069
            })
1070
            ->where('i_file', '=', $this->tree->id())
1071
            ->where('birth.d_fact', '=', 'BIRT')
1072
            ->where('death.d_fact', '=', 'DEAT')
1073
            ->whereIn('birth.d_type', ['@#DGREGORIAN@', '@#DJULIAN@'])
1074
            ->whereIn('death.d_type', ['@#DGREGORIAN@', '@#DJULIAN@'])
1075
            ->whereColumn('death.d_julianday1', '>=', 'birth.d_julianday2')
1076
            ->where('birth.d_julianday2', '<>', 0)
1077
            ->groupBy(['century', 'sex'])
1078
            ->orderBy('century')
1079
            ->orderBy('sex')
1080
            ->get()
1081
            ->map(static fn (object $row): object => (object) [
1082
                'age'     => (float) $row->age,
1083
                'century' => (int) $row->century,
1084
                'sex'     => $row->sex,
1085
            ])
1086
            ->all();
1087
    }
1088
1089
    /**
1090
     * General query on ages.
1091
     *
1092
     * @return array<object{days:int}>
0 ignored issues
show
Documentation Bug introduced by
The doc comment array<object{days:int}> at position 2 could not be parsed: Expected '>' at position 2, but found 'object'.
Loading history...
1093
     */
1094
    public function statsAgeQuery(string $sex, int $year1, int $year2): array
1095
    {
1096
        $query = $this->birthAndDeathQuery($sex);
1097
1098
        if ($year1 !== 0 && $year2 !== 0) {
1099
            $query
1100
                ->whereIn('birth.d_type', ['@#DGREGORIAN@', '@#DJULIAN@'])
1101
                ->whereIn('death.d_type', ['@#DGREGORIAN@', '@#DJULIAN@'])
1102
                ->whereBetween('death.d_year', [$year1, $year2]);
1103
        }
1104
1105
        return $query
1106
            ->select([new Expression(DB::prefix('death.d_julianday2') . ' - ' . DB::prefix('birth.d_julianday1') . ' AS days')])
1107
            ->orderBy('days', 'desc')
1108
            ->get()
1109
            ->map(static fn (object $row): object => (object) ['days' => (int) $row->days])
1110
            ->all();
1111
    }
1112
1113
    private function birthAndDeathQuery(string $sex): Builder
1114
    {
1115
        $query = DB::table('individuals')
1116
            ->where('i_file', '=', $this->tree->id())
1117
            ->join('dates AS birth', static function (JoinClause $join): void {
1118
                $join
1119
                    ->on('birth.d_file', '=', 'i_file')
1120
                    ->on('birth.d_gid', '=', 'i_id');
1121
            })
1122
            ->join('dates AS death', static function (JoinClause $join): void {
1123
                $join
1124
                    ->on('death.d_file', '=', 'i_file')
1125
                    ->on('death.d_gid', '=', 'i_id');
1126
            })
1127
            ->where('birth.d_fact', '=', 'BIRT')
1128
            ->where('death.d_fact', '=', 'DEAT')
1129
            ->whereColumn('death.d_julianday1', '>=', 'birth.d_julianday2')
1130
            ->where('birth.d_julianday2', '<>', 0);
1131
1132
        if ($sex !== 'ALL') {
1133
            $query->where('i_sex', '=', $sex);
1134
        }
1135
1136
        return $query;
1137
    }
1138
1139
    /**
1140
     * @return object{individual:Individual,days:int}|null
1141
     */
1142
    public function longlifeQuery(string $sex): ?object
1143
    {
1144
        return $this->birthAndDeathQuery($sex)
1145
            ->orderBy('days', 'desc')
1146
            ->select(['individuals.*', new Expression(DB::prefix('death.d_julianday2') . ' - ' . DB::prefix('birth.d_julianday1') . ' AS days')])
1147
            ->take(1)
1148
            ->get()
1149
            ->map(fn (object $row): object => (object) [
1150
                'individual' => Registry::individualFactory()->mapper($this->tree)($row),
1151
                'days'       => (int) $row->days
1152
            ])
1153
            ->first();
1154
    }
1155
1156
    /**
1157
     * @return Collection<int,object{individual:Individual,days:int}>
1158
     */
1159
    public function topTenOldestQuery(string $sex, int $limit): Collection
1160
    {
1161
        return $this->birthAndDeathQuery($sex)
1162
            ->groupBy(['i_id', 'i_file'])
1163
            ->orderBy('days', 'desc')
1164
            ->select(['individuals.*', new Expression('MAX(' . DB::prefix('death.d_julianday2') . ' - ' . DB::prefix('birth.d_julianday1') . ') AS days')])
1165
            ->take($limit)
1166
            ->get()
1167
            ->map(fn (object $row): object => (object) [
1168
                'individual' => Registry::individualFactory()->mapper($this->tree)($row),
1169
                'days'       => (int) $row->days
1170
            ]);
1171
    }
1172
1173
    /**
1174
     * @return array<string>
1175
     */
1176
    private function getIso3166Countries(): array
1177
    {
1178
        // Get the country names for each language
1179
        $country_to_iso3166 = [];
1180
1181
        $current_language = I18N::languageTag();
1182
1183
        foreach (I18N::activeLocales() as $locale) {
1184
            I18N::init($locale->languageTag());
1185
1186
            $countries = $this->getAllCountries();
1187
1188
            foreach ($this->iso3166() as $three => $two) {
1189
                $country_to_iso3166[$three]             = $two;
1190
                $country_to_iso3166[$countries[$three]] = $two;
1191
            }
1192
        }
1193
1194
        I18N::init($current_language);
1195
1196
        return $country_to_iso3166;
1197
    }
1198
1199
    /**
1200
     * Returns the data structure required by google geochart.
1201
     *
1202
     * @param array<int> $places
1203
     *
1204
     * @return array<int,array<int|string|array<string,string>>>
1205
     */
1206
    private function createChartData(array $places): array
1207
    {
1208
        $data = [
1209
            [
1210
                I18N::translate('Country'),
1211
                I18N::translate('Total'),
1212
            ],
1213
        ];
1214
1215
        // webtrees uses 3-letter country codes and localised country names, but google uses 2 letter codes.
1216
        foreach ($places as $country => $count) {
1217
            $data[] = [
1218
                [
1219
                    'v' => $country,
1220
                    'f' => $this->mapTwoLetterToName($country),
1221
                ],
1222
                $count
1223
            ];
1224
        }
1225
1226
        return $data;
1227
    }
1228
1229
    /**
1230
     * @return array<string,int>
1231
     */
1232
    private function countIndividualsByCountry(Tree $tree): array
1233
    {
1234
        $rows = DB::table('places')
1235
            ->where('p_file', '=', $tree->id())
1236
            ->where('p_parent_id', '=', 0)
1237
            ->join('placelinks', static function (JoinClause $join): void {
1238
                $join
1239
                    ->on('pl_file', '=', 'p_file')
1240
                    ->on('pl_p_id', '=', 'p_id');
1241
            })
1242
            ->join('individuals', static function (JoinClause $join): void {
1243
                $join
1244
                    ->on('pl_file', '=', 'i_file')
1245
                    ->on('pl_gid', '=', 'i_id');
1246
            })
1247
            ->groupBy('p_place')
1248
            ->pluck(new Expression('COUNT(*)'), 'p_place')
1249
            ->all();
1250
1251
        $totals = [];
1252
1253
        $country_to_iso3166 = $this->getIso3166Countries();
1254
1255
        foreach ($rows as $country => $count) {
1256
            $country_code = $country_to_iso3166[$country] ?? null;
1257
1258
            if ($country_code !== null) {
1259
                $totals[$country_code] ??= 0;
1260
                $totals[$country_code] += $count;
1261
            }
1262
        }
1263
1264
        return $totals;
1265
    }
1266
1267
    /**
1268
     * @return array<string,int>
1269
     */
1270
    private function countSurnamesByCountry(Tree $tree, string $surname): array
1271
    {
1272
        $rows =
1273
            DB::table('places')
1274
                ->where('p_file', '=', $tree->id())
1275
                ->where('p_parent_id', '=', 0)
1276
                ->join('placelinks', static function (JoinClause $join): void {
1277
                    $join
1278
                        ->on('pl_file', '=', 'p_file')
1279
                        ->on('pl_p_id', '=', 'p_id');
1280
                })
1281
                ->join('name', static function (JoinClause $join): void {
1282
                    $join
1283
                        ->on('n_file', '=', 'pl_file')
1284
                        ->on('n_id', '=', 'pl_gid');
1285
                })
1286
                ->where('n_surn', '=', $surname)
1287
                ->groupBy('p_place')
1288
                ->pluck(new Expression('COUNT(*)'), 'p_place');
1289
1290
        $totals = [];
1291
1292
        $country_to_iso3166 = $this->getIso3166Countries();
1293
1294
        foreach ($rows as $country => $count) {
1295
            $country_code = $country_to_iso3166[$country] ?? null;
1296
1297
            if ($country_code !== null) {
1298
                $totals[$country_code] ??= 0;
1299
                $totals[$country_code] += $count;
1300
            }
1301
        }
1302
1303
        return $totals;
1304
    }
1305
1306
    /**
1307
     * @return array<string,int>
1308
     */
1309
    private function countFamilyEventsByCountry(Tree $tree, string $fact): array
1310
    {
1311
        $query = DB::table('places')
1312
            ->where('p_file', '=', $tree->id())
1313
            ->where('p_parent_id', '=', 0)
1314
            ->join('placelinks', static function (JoinClause $join): void {
1315
                $join
1316
                    ->on('pl_file', '=', 'p_file')
1317
                    ->on('pl_p_id', '=', 'p_id');
1318
            })
1319
            ->join('families', static function (JoinClause $join): void {
1320
                $join
1321
                    ->on('pl_file', '=', 'f_file')
1322
                    ->on('pl_gid', '=', 'f_id');
1323
            })
1324
            ->select(['p_place AS place', 'f_gedcom AS gedcom']);
1325
1326
        return $this->filterEventPlaces($query, $fact);
1327
    }
1328
1329
    /**
1330
     * @return array<string,int>
1331
     */
1332
    private function countIndividualEventsByCountry(Tree $tree, string $fact): array
1333
    {
1334
        $query = DB::table('places')
1335
            ->where('p_file', '=', $tree->id())
1336
            ->where('p_parent_id', '=', 0)
1337
            ->join('placelinks', static function (JoinClause $join): void {
1338
                $join
1339
                    ->on('pl_file', '=', 'p_file')
1340
                    ->on('pl_p_id', '=', 'p_id');
1341
            })
1342
            ->join('individuals', static function (JoinClause $join): void {
1343
                $join
1344
                    ->on('pl_file', '=', 'i_file')
1345
                    ->on('pl_gid', '=', 'i_id');
1346
            })
1347
            ->select(['p_place AS place', 'i_gedcom AS gedcom']);
1348
1349
        return $this->filterEventPlaces($query, $fact);
1350
    }
1351
1352
    /**
1353
     * @return array<string,int>
1354
     */
1355
    private function filterEventPlaces(Builder $query, string $fact): array
1356
    {
1357
        $totals = [];
1358
1359
        $country_to_iso3166 = $this->getIso3166Countries();
1360
1361
        foreach ($query->cursor() as $row) {
1362
            $country_code = $country_to_iso3166[$row->place] ?? null;
1363
1364
            if ($country_code !== null) {
1365
                $place_regex = '/\n1 ' . $fact . '(?:\n[2-9].*)*\n2 PLAC.*[, ]' . preg_quote($row->place, '(?:\n|$)/i') . '\n/';
1366
1367
                if (preg_match($place_regex, $row->gedcom) === 1) {
1368
                    $totals[$country_code] = 1 + ($totals[$country_code] ?? 0);
1369
                }
1370
            }
1371
        }
1372
1373
        return $totals;
1374
    }
1375
1376
    /**
1377
     * Create a chart showing where events occurred.
1378
     *
1379
     * @param string $chart_shows The type of chart map to show
1380
     * @param string $chart_type  The type of chart to show
1381
     * @param string $surname     The surname for surname based distribution chart
1382
     */
1383
    public function chartDistribution(
1384
        string $chart_shows = 'world',
1385
        string $chart_type = '',
1386
        string $surname = ''
1387
    ): string {
1388
        switch ($chart_type) {
1389
            case 'surname_distribution_chart':
1390
                $chart_title = I18N::translate('Surname distribution chart') . ': ' . $surname;
1391
                $surnames    = $this->commonSurnames(1, 0, 'count');
1392
                $surname     = implode(I18N::$list_separator, array_keys(array_shift($surnames) ?? []));
1393
                $data        = $this->createChartData($this->countSurnamesByCountry($this->tree, $surname));
1394
                break;
1395
1396
            case 'birth_distribution_chart':
1397
                $chart_title = I18N::translate('Birth by country');
1398
                $data        = $this->createChartData($this->countIndividualEventsByCountry($this->tree, 'BIRT'));
1399
                break;
1400
1401
            case 'death_distribution_chart':
1402
                $chart_title = I18N::translate('Death by country');
1403
                $data        = $this->createChartData($this->countIndividualEventsByCountry($this->tree, 'DEAT'));
1404
                break;
1405
1406
            case 'marriage_distribution_chart':
1407
                $chart_title = I18N::translate('Marriage by country');
1408
                $data        = $this->createChartData($this->countFamilyEventsByCountry($this->tree, 'MARR'));
1409
                break;
1410
1411
            case 'indi_distribution_chart':
1412
            default:
1413
                $chart_title = I18N::translate('Individual distribution chart');
1414
                $data        = $this->createChartData($this->countIndividualsByCountry($this->tree));
1415
                break;
1416
        }
1417
1418
        return view('statistics/other/charts/geo', [
1419
            'chart_title'  => $chart_title,
1420
            'chart_color2' => '84beff',
1421
            'chart_color3' => 'c3dfff',
1422
            'region'       => $chart_shows,
1423
            'data'         => $data,
1424
            'language'     => I18N::languageTag(),
1425
        ]);
1426
    }
1427
1428
    /**
1429
     * @return array<array{family:Family,count:int}>
1430
     */
1431
    private function topTenGrandFamilyQuery(int $limit): array
1432
    {
1433
        return DB::table('families')
1434
            ->join('link AS children', static function (JoinClause $join): void {
1435
                $join
1436
                    ->on('children.l_from', '=', 'f_id')
1437
                    ->on('children.l_file', '=', 'f_file')
1438
                    ->where('children.l_type', '=', 'CHIL');
1439
            })->join('link AS mchildren', static function (JoinClause $join): void {
1440
                $join
1441
                    ->on('mchildren.l_file', '=', 'children.l_file')
1442
                    ->on('mchildren.l_from', '=', 'children.l_to')
1443
                    ->where('mchildren.l_type', '=', 'FAMS');
1444
            })->join('link AS gchildren', static function (JoinClause $join): void {
1445
                $join
1446
                    ->on('gchildren.l_file', '=', 'mchildren.l_file')
1447
                    ->on('gchildren.l_from', '=', 'mchildren.l_to')
1448
                    ->where('gchildren.l_type', '=', 'CHIL');
1449
            })
1450
            ->where('f_file', '=', $this->tree->id())
1451
            ->groupBy(['f_id', 'f_file'])
1452
            ->orderBy(new Expression('COUNT(*)'), 'DESC')
1453
            ->select(['families.*'])
1454
            ->limit($limit)
1455
            ->get()
1456
            ->map(Registry::familyFactory()->mapper($this->tree))
1457
            ->filter(GedcomRecord::accessFilter())
1458
            ->map(static function (Family $family): array {
1459
                $count = 0;
1460
                foreach ($family->children() as $child) {
1461
                    foreach ($child->spouseFamilies() as $spouse_family) {
1462
                        $count += $spouse_family->children()->count();
1463
                    }
1464
                }
1465
1466
                return [
1467
                    'family' => $family,
1468
                    'count'  => $count,
1469
                ];
1470
            })
1471
            ->all();
1472
    }
1473
1474
    public function topTenLargestGrandFamily(int $limit = 10): string
1475
    {
1476
        return view('statistics/families/top10-nolist-grand', [
1477
            'records' => $this->topTenGrandFamilyQuery($limit),
1478
        ]);
1479
    }
1480
1481
    public function topTenLargestGrandFamilyList(int $limit = 10): string
1482
    {
1483
        return view('statistics/families/top10-list-grand', [
1484
            'records' => $this->topTenGrandFamilyQuery($limit),
1485
        ]);
1486
    }
1487
1488
    public function noChildrenFamiliesList(string $type = 'list'): string
1489
    {
1490
        $families = DB::table('families')
1491
            ->where('f_file', '=', $this->tree->id())
1492
            ->where('f_numchil', '=', 0)
1493
            ->get()
1494
            ->map(Registry::familyFactory()->mapper($this->tree))
1495
            ->filter(GedcomRecord::accessFilter());
1496
1497
        $top10 = [];
1498
1499
        foreach ($families as $family) {
1500
            if ($type === 'list') {
1501
                $top10[] = '<li><a href="' . e($family->url()) . '">' . $family->fullName() . '</a></li>';
1502
            } else {
1503
                $top10[] = '<a href="' . e($family->url()) . '">' . $family->fullName() . '</a>';
1504
            }
1505
        }
1506
1507
        if ($type === 'list') {
1508
            $top10 = implode('', $top10);
1509
        } else {
1510
            $top10 = implode('; ', $top10);
1511
        }
1512
1513
        if ($type === 'list') {
1514
            return '<ul>' . $top10 . '</ul>';
1515
        }
1516
1517
        return $top10;
1518
    }
1519
1520
    /**
1521
     * Returns the calculated age the time of event.
1522
     *
1523
     * @param int $age The age from the database record
1524
     */
1525
    private function calculateAge(int $age): string
1526
    {
1527
        if ($age < 31) {
1528
            return I18N::plural('%s day', '%s days', $age, I18N::number($age));
1529
        }
1530
1531
        if ($age < 365) {
1532
            $months = (int) ($age / 30.5);
1533
1534
            return I18N::plural('%s month', '%s months', $months, I18N::number($months));
1535
        }
1536
1537
        $years = (int) ($age / 365.25);
1538
1539
        return I18N::plural('%s year', '%s years', $years, I18N::number($years));
1540
    }
1541
1542
    public function topAgeBetweenSiblings(): string
1543
    {
1544
        $rows = $this->maximumAgeBetweenSiblings(1);
1545
1546
        if ($rows === []) {
1547
            return I18N::translate('This information is not available.');
1548
        }
1549
1550
        return $rows[0]->age;
1551
    }
1552
1553
    public function topAgeBetweenSiblingsFullName(): string
1554
    {
1555
        $rows = $this->maximumAgeBetweenSiblings(1);
1556
1557
        if ($rows === []) {
1558
            return I18N::translate('This information is not available.');
1559
        }
1560
1561
        return view('statistics/families/top10-nolist-age', ['record' => (array) $rows[0]]);
1562
    }
1563
1564
    public function topAgeBetweenSiblingsList(int $limit, bool $unique_families): string
1565
    {
1566
        $rows    = $this->maximumAgeBetweenSiblings($limit);
1567
        $records = [];
1568
        $dist    = [];
1569
1570
        foreach ($rows as $row) {
1571
            if (!$unique_families || !in_array($row->family, $dist, true)) {
1572
                $records[] = [
1573
                    'child1' => $row->child1,
1574
                    'child2' => $row->child2,
1575
                    'family' => $row->family,
1576
                    'age'    => $row->age,
1577
                ];
1578
1579
                $dist[] = $row->family;
1580
            }
1581
        }
1582
1583
        return view('statistics/families/top10-list-age', [
1584
            'records' => $records,
1585
        ]);
1586
    }
1587
1588
    /**
1589
     * @return array<object{f_numchil:int,total:int}>
0 ignored issues
show
Documentation Bug introduced by
The doc comment array<object{f_numchil:int,total:int}> at position 2 could not be parsed: Expected '>' at position 2, but found 'object'.
Loading history...
1590
     */
1591
    public function statsChildrenQuery(int $year1, int $year2): array
1592
    {
1593
        $query = DB::table('families')
1594
            ->where('f_file', '=', $this->tree->id())
1595
            ->groupBy(['f_numchil'])
1596
            ->select(['f_numchil', new Expression('COUNT(*) AS total')]);
1597
1598
        if ($year1 !== 0 && $year2 !== 0) {
1599
            $query
1600
                ->join('dates', static function (JoinClause $join): void {
1601
                    $join
1602
                        ->on('d_file', '=', 'f_file')
1603
                        ->on('d_gid', '=', 'f_id');
1604
                })
1605
                ->where('d_fact', '=', 'MARR')
1606
                ->whereIn('d_type', ['@#DGREGORIAN@', '@#DJULIAN@'])
1607
                ->whereBetween('d_year', [$year1, $year2]);
1608
        }
1609
1610
        return $query->get()
1611
            ->map(static fn (object $row): object => (object) [
1612
                'f_numchil' => (int) $row->f_numchil,
1613
                'total'     => (int) $row->total,
1614
            ])
1615
            ->all();
1616
    }
1617
1618
    /**
1619
     * @return array<array{family:Family,count:int}>
1620
     */
1621
    private function topTenFamilyQuery(int $limit): array
1622
    {
1623
        return DB::table('families')
1624
            ->where('f_file', '=', $this->tree->id())
1625
            ->orderBy('f_numchil', 'DESC')
1626
            ->limit($limit)
1627
            ->get()
1628
            ->map(Registry::familyFactory()->mapper($this->tree))
1629
            ->filter(GedcomRecord::accessFilter())
1630
            ->map(static fn (Family $family): array => [
1631
                'family' => $family,
1632
                'count'  => $family->numberOfChildren(),
1633
            ])
1634
            ->all();
1635
    }
1636
1637
    public function topTenLargestFamily(int $limit = 10): string
1638
    {
1639
        $records = $this->topTenFamilyQuery($limit);
1640
1641
        return view('statistics/families/top10-nolist', [
1642
            'records' => $records,
1643
        ]);
1644
    }
1645
1646
    public function topTenLargestFamilyList(int $limit = 10): string
1647
    {
1648
        $records = $this->topTenFamilyQuery($limit);
1649
1650
        return view('statistics/families/top10-list', [
1651
            'records' => $records,
1652
        ]);
1653
    }
1654
1655
    public function parentsQuery(string $type, string $age_dir, string $sex, bool $show_years): string
1656
    {
1657
        if ($sex === 'F') {
1658
            $sex_field = 'WIFE';
1659
        } else {
1660
            $sex_field = 'HUSB';
1661
        }
1662
1663
        if ($age_dir !== 'ASC') {
1664
            $age_dir = 'DESC';
1665
        }
1666
1667
        $row = DB::table('link AS parentfamily')
1668
            ->join('link AS childfamily', static function (JoinClause $join): void {
1669
                $join
1670
                    ->on('childfamily.l_file', '=', 'parentfamily.l_file')
1671
                    ->on('childfamily.l_from', '=', 'parentfamily.l_from')
1672
                    ->where('childfamily.l_type', '=', 'CHIL');
1673
            })
1674
            ->join('dates AS birth', static function (JoinClause $join): void {
1675
                $join
1676
                    ->on('birth.d_file', '=', 'parentfamily.l_file')
1677
                    ->on('birth.d_gid', '=', 'parentfamily.l_to')
1678
                    ->where('birth.d_fact', '=', 'BIRT')
1679
                    ->where('birth.d_julianday1', '<>', 0);
1680
            })
1681
            ->join('dates AS childbirth', static function (JoinClause $join): void {
1682
                $join
1683
                    ->on('childbirth.d_file', '=', 'parentfamily.l_file')
1684
                    ->on('childbirth.d_gid', '=', 'childfamily.l_to')
1685
                    ->where('childbirth.d_fact', '=', 'BIRT');
1686
            })
1687
            ->where('childfamily.l_file', '=', $this->tree->id())
1688
            ->where('parentfamily.l_type', '=', $sex_field)
1689
            ->where('childbirth.d_julianday2', '>', new Expression(DB::prefix('birth.d_julianday1')))
1690
            ->select(['parentfamily.l_to AS id', new Expression(DB::prefix('childbirth.d_julianday2') . ' - ' . DB::prefix('birth.d_julianday1') . ' AS age')])
1691
            ->take(1)
1692
            ->orderBy('age', $age_dir)
1693
            ->get()
1694
            ->first();
1695
1696
        if ($row === null) {
1697
            return I18N::translate('This information is not available.');
1698
        }
1699
1700
        $person = Registry::individualFactory()->make($row->id, $this->tree);
1701
1702
        switch ($type) {
1703
            default:
1704
            case 'full':
1705
                if ($person !== null && $person->canShow()) {
1706
                    $result = $person->formatList();
1707
                } else {
1708
                    $result = I18N::translate('This information is private and cannot be shown.');
1709
                }
1710
                break;
1711
1712
            case 'name':
1713
                $result = '<a href="' . e($person->url()) . '">' . $person->fullName() . '</a>';
1714
                break;
1715
1716
            case 'age':
1717
                $age = $row->age;
1718
1719
                if ($show_years) {
1720
                    $result = $this->calculateAge((int) $row->age);
1721
                } else {
1722
                    $result = (string) floor($age / 365.25);
1723
                }
1724
1725
                break;
1726
        }
1727
1728
        return $result;
1729
    }
1730
1731
    /**
1732
     * General query on age at marriage.
1733
     *
1734
     * @param string $type
1735
     * @param string $age_dir "ASC" or "DESC"
1736
     * @param int    $limit
1737
     */
1738
    public function ageOfMarriageQuery(string $type, string $age_dir, int $limit): string
1739
    {
1740
        $hrows = DB::table('families')
1741
            ->where('f_file', '=', $this->tree->id())
1742
            ->join('dates AS married', static function (JoinClause $join): void {
1743
                $join
1744
                    ->on('married.d_file', '=', 'f_file')
1745
                    ->on('married.d_gid', '=', 'f_id')
1746
                    ->where('married.d_fact', '=', 'MARR')
1747
                    ->where('married.d_julianday1', '<>', 0);
1748
            })
1749
            ->join('dates AS husbdeath', static function (JoinClause $join): void {
1750
                $join
1751
                    ->on('husbdeath.d_gid', '=', 'f_husb')
1752
                    ->on('husbdeath.d_file', '=', 'f_file')
1753
                    ->where('husbdeath.d_fact', '=', 'DEAT');
1754
            })
1755
            ->whereColumn('married.d_julianday1', '<', 'husbdeath.d_julianday2')
1756
            ->groupBy(['f_id'])
1757
            ->select(['f_id AS family', new Expression('MIN(' . DB::prefix('husbdeath.d_julianday2') . ' - ' . DB::prefix('married.d_julianday1') . ') AS age')])
1758
            ->get()
1759
            ->all();
1760
1761
        $wrows = DB::table('families')
1762
            ->where('f_file', '=', $this->tree->id())
1763
            ->join('dates AS married', static function (JoinClause $join): void {
1764
                $join
1765
                    ->on('married.d_file', '=', 'f_file')
1766
                    ->on('married.d_gid', '=', 'f_id')
1767
                    ->where('married.d_fact', '=', 'MARR')
1768
                    ->where('married.d_julianday1', '<>', 0);
1769
            })
1770
            ->join('dates AS wifedeath', static function (JoinClause $join): void {
1771
                $join
1772
                    ->on('wifedeath.d_gid', '=', 'f_wife')
1773
                    ->on('wifedeath.d_file', '=', 'f_file')
1774
                    ->where('wifedeath.d_fact', '=', 'DEAT');
1775
            })
1776
            ->whereColumn('married.d_julianday1', '<', 'wifedeath.d_julianday2')
1777
            ->groupBy(['f_id'])
1778
            ->select(['f_id AS family', new Expression('MIN(' . DB::prefix('wifedeath.d_julianday2') . ' - ' . DB::prefix('married.d_julianday1') . ') AS age')])
1779
            ->get()
1780
            ->all();
1781
1782
        $drows = DB::table('families')
1783
            ->where('f_file', '=', $this->tree->id())
1784
            ->join('dates AS married', static function (JoinClause $join): void {
1785
                $join
1786
                    ->on('married.d_file', '=', 'f_file')
1787
                    ->on('married.d_gid', '=', 'f_id')
1788
                    ->where('married.d_fact', '=', 'MARR')
1789
                    ->where('married.d_julianday1', '<>', 0);
1790
            })
1791
            ->join('dates AS divorced', static function (JoinClause $join): void {
1792
                $join
1793
                    ->on('divorced.d_gid', '=', 'f_id')
1794
                    ->on('divorced.d_file', '=', 'f_file')
1795
                    ->whereIn('divorced.d_fact', ['DIV', 'ANUL', '_SEPR']);
1796
            })
1797
            ->whereColumn('married.d_julianday1', '<', 'divorced.d_julianday2')
1798
            ->groupBy(['f_id'])
1799
            ->select(['f_id AS family', new Expression('MIN(' . DB::prefix('divorced.d_julianday2') . ' - ' . DB::prefix('married.d_julianday1') . ') AS age')])
1800
            ->get()
1801
            ->all();
1802
1803
        $rows = [];
1804
        foreach ($drows as $family) {
1805
            $rows[$family->family] = $family->age;
1806
        }
1807
1808
        foreach ($hrows as $family) {
1809
            if (!isset($rows[$family->family])) {
1810
                $rows[$family->family] = $family->age;
1811
            }
1812
        }
1813
1814
        foreach ($wrows as $family) {
1815
            if (!isset($rows[$family->family])) {
1816
                $rows[$family->family] = $family->age;
1817
            } elseif ($rows[$family->family] > $family->age) {
1818
                $rows[$family->family] = $family->age;
1819
            }
1820
        }
1821
1822
        if ($age_dir === 'DESC') {
1823
            arsort($rows);
1824
        } else {
1825
            asort($rows);
1826
        }
1827
1828
        $top10 = [];
1829
        $i     = 0;
1830
        foreach ($rows as $xref => $age) {
1831
            $family = Registry::familyFactory()->make((string) $xref, $this->tree);
1832
            if ($type === 'name') {
1833
                return $family->formatList();
1834
            }
1835
1836
            $age = $this->calculateAge((int) $age);
1837
1838
            if ($type === 'age') {
1839
                return $age;
1840
            }
1841
1842
            $husb = $family->husband();
1843
            $wife = $family->wife();
1844
1845
            if (
1846
                $husb instanceof Individual &&
1847
                $wife instanceof Individual &&
1848
                ($husb->getAllDeathDates() || !$husb->isDead()) &&
1849
                ($wife->getAllDeathDates() || !$wife->isDead())
1850
            ) {
1851
                if ($family->canShow()) {
1852
                    if ($type === 'list') {
1853
                        $top10[] = '<li><a href="' . e($family->url()) . '">' . $family->fullName() . '</a> (' . $age . ')' . '</li>';
1854
                    } else {
1855
                        $top10[] = '<a href="' . e($family->url()) . '">' . $family->fullName() . '</a> (' . $age . ')';
1856
                    }
1857
                }
1858
                if (++$i === $limit) {
1859
                    break;
1860
                }
1861
            }
1862
        }
1863
1864
        if ($type === 'list') {
1865
            $top10 = implode('', $top10);
1866
        } else {
1867
            $top10 = implode('; ', $top10);
1868
        }
1869
1870
        if (I18N::direction() === 'rtl') {
1871
            $top10 = str_replace([
1872
                '[',
1873
                ']',
1874
                '(',
1875
                ')',
1876
                '+',
1877
            ], [
1878
                '&rlm;[',
1879
                '&rlm;]',
1880
                '&rlm;(',
1881
                '&rlm;)',
1882
                '&rlm;+',
1883
            ], $top10);
1884
        }
1885
1886
        if ($type === 'list') {
1887
            return '<ul>' . $top10 . '</ul>';
1888
        }
1889
1890
        return $top10;
1891
    }
1892
1893
    /**
1894
     * @return array<array{family:Family,age:string}>
1895
     */
1896
    private function ageBetweenSpousesQuery(string $age_dir, int $limit): array
1897
    {
1898
        $query = DB::table('families')
1899
            ->where('f_file', '=', $this->tree->id())
1900
            ->join('dates AS wife', static function (JoinClause $join): void {
1901
                $join
1902
                    ->on('wife.d_gid', '=', 'f_wife')
1903
                    ->on('wife.d_file', '=', 'f_file')
1904
                    ->where('wife.d_fact', '=', 'BIRT')
1905
                    ->where('wife.d_julianday1', '<>', 0);
1906
            })
1907
            ->join('dates AS husb', static function (JoinClause $join): void {
1908
                $join
1909
                    ->on('husb.d_gid', '=', 'f_husb')
1910
                    ->on('husb.d_file', '=', 'f_file')
1911
                    ->where('husb.d_fact', '=', 'BIRT')
1912
                    ->where('husb.d_julianday1', '<>', 0);
1913
            });
1914
1915
        if ($age_dir === 'DESC') {
1916
            $query
1917
                ->whereColumn('wife.d_julianday1', '>=', 'husb.d_julianday1')
1918
                ->orderBy(new Expression('MIN(' . DB::prefix('wife.d_julianday1') . ') - MIN(' . DB::prefix('husb.d_julianday1') . ')'), 'DESC');
1919
        } else {
1920
            $query
1921
                ->whereColumn('husb.d_julianday1', '>=', 'wife.d_julianday1')
1922
                ->orderBy(new Expression('MIN(' . DB::prefix('husb.d_julianday1') . ') - MIN(' . DB::prefix('wife.d_julianday1') . ')'), 'DESC');
1923
        }
1924
1925
        return $query
1926
            ->groupBy(['f_id', 'f_file'])
1927
            ->select(['families.*'])
1928
            ->take($limit)
1929
            ->get()
1930
            ->map(Registry::familyFactory()->mapper($this->tree))
1931
            ->filter(GedcomRecord::accessFilter())
1932
            ->map(function (Family $family) use ($age_dir): array {
1933
                $husb_birt_jd = $family->husband()->getBirthDate()->minimumJulianDay();
1934
                $wife_birt_jd = $family->wife()->getBirthDate()->minimumJulianDay();
1935
1936
                if ($age_dir === 'DESC') {
1937
                    $diff = $wife_birt_jd - $husb_birt_jd;
1938
                } else {
1939
                    $diff = $husb_birt_jd - $wife_birt_jd;
1940
                }
1941
1942
                return [
1943
                    'family' => $family,
1944
                    'age'    => $this->calculateAge($diff),
1945
                ];
1946
            })
1947
            ->all();
1948
    }
1949
1950
    public function ageBetweenSpousesMF(int $limit = 10): string
1951
    {
1952
        $records = $this->ageBetweenSpousesQuery('DESC', $limit);
1953
1954
        return view('statistics/families/top10-nolist-spouses', [
1955
            'records' => $records,
1956
        ]);
1957
    }
1958
1959
    public function ageBetweenSpousesMFList(int $limit = 10): string
1960
    {
1961
        $records = $this->ageBetweenSpousesQuery('DESC', $limit);
1962
1963
        return view('statistics/families/top10-list-spouses', [
1964
            'records' => $records,
1965
        ]);
1966
    }
1967
1968
    public function ageBetweenSpousesFM(int $limit = 10): string
1969
    {
1970
        return view('statistics/families/top10-nolist-spouses', [
1971
            'records' => $this->ageBetweenSpousesQuery('ASC', $limit),
1972
        ]);
1973
    }
1974
1975
    public function ageBetweenSpousesFMList(int $limit = 10): string
1976
    {
1977
        return view('statistics/families/top10-list-spouses', [
1978
            'records' => $this->ageBetweenSpousesQuery('ASC', $limit),
1979
        ]);
1980
    }
1981
1982
    /**
1983
     * @return array<object{f_id:string,d_gid:string,age:int}>
0 ignored issues
show
Documentation Bug introduced by
The doc comment array<object{f_id:string,d_gid:string,age:int}> at position 2 could not be parsed: Expected '>' at position 2, but found 'object'.
Loading history...
1984
     */
1985
    public function statsMarrAgeQuery(string $sex, int $year1, int $year2): array
1986
    {
1987
        $query = DB::table('dates AS married')
1988
            ->join('families', static function (JoinClause $join): void {
1989
                $join
1990
                    ->on('f_file', '=', 'married.d_file')
1991
                    ->on('f_id', '=', 'married.d_gid');
1992
            })
1993
            ->join('dates AS birth', static function (JoinClause $join) use ($sex): void {
1994
                $join
1995
                    ->on('birth.d_file', '=', 'married.d_file')
1996
                    ->on('birth.d_gid', '=', $sex === 'M' ? 'f_husb' : 'f_wife')
1997
                    ->where('birth.d_julianday1', '<>', 0)
1998
                    ->where('birth.d_fact', '=', 'BIRT')
1999
                    ->whereIn('birth.d_type', ['@#DGREGORIAN@', '@#DJULIAN@']);
2000
            })
2001
            ->where('married.d_file', '=', $this->tree->id())
2002
            ->where('married.d_fact', '=', 'MARR')
2003
            ->whereIn('married.d_type', ['@#DGREGORIAN@', '@#DJULIAN@'])
2004
            ->whereColumn('married.d_julianday1', '>', 'birth.d_julianday1')
2005
            ->select(['f_id', 'birth.d_gid', new Expression(DB::prefix('married.d_julianday2') . ' - ' . DB::prefix('birth.d_julianday1') . ' AS age')]);
2006
2007
        if ($year1 !== 0 && $year2 !== 0) {
2008
            $query->whereBetween('married.d_year', [$year1, $year2]);
2009
        }
2010
2011
        return $query
2012
            ->get()
2013
            ->map(static fn (object $row): object => (object) [
2014
                'f_id'  => $row->f_id,
2015
                'd_gid' => $row->d_gid,
2016
                'age'   => (int) $row->age,
2017
            ])
2018
            ->all();
2019
    }
2020
2021
    /**
2022
     * Query the database for marriage tags.
2023
     *
2024
     * @param string $show    "full", "name" or "age"
2025
     * @param string $age_dir "ASC" or "DESC"
2026
     * @param string $sex     "F" or "M"
2027
     * @param bool   $show_years
2028
     */
2029
    public function marriageQuery(string $show, string $age_dir, string $sex, bool $show_years): string
2030
    {
2031
        if ($sex === 'F') {
2032
            $sex_field = 'f_wife';
2033
        } else {
2034
            $sex_field = 'f_husb';
2035
        }
2036
2037
        if ($age_dir !== 'ASC') {
2038
            $age_dir = 'DESC';
2039
        }
2040
2041
        $row = DB::table('families')
2042
            ->join('dates AS married', static function (JoinClause $join): void {
2043
                $join
2044
                    ->on('married.d_file', '=', 'f_file')
2045
                    ->on('married.d_gid', '=', 'f_id')
2046
                    ->where('married.d_fact', '=', 'MARR');
2047
            })
2048
            ->join('individuals', static function (JoinClause $join) use ($sex, $sex_field): void {
2049
                $join
2050
                    ->on('i_file', '=', 'f_file')
2051
                    ->on('i_id', '=', $sex_field)
2052
                    ->where('i_sex', '=', $sex);
2053
            })
2054
            ->join('dates AS birth', static function (JoinClause $join): void {
2055
                $join
2056
                    ->on('birth.d_file', '=', 'i_file')
2057
                    ->on('birth.d_gid', '=', 'i_id')
2058
                    ->where('birth.d_fact', '=', 'BIRT')
2059
                    ->where('birth.d_julianday1', '<>', 0);
2060
            })
2061
            ->where('f_file', '=', $this->tree->id())
2062
            ->where('married.d_julianday2', '>', new Expression(DB::prefix('birth.d_julianday1')))
2063
            ->orderBy(new Expression(DB::prefix('married.d_julianday2') . ' - ' . DB::prefix('birth.d_julianday1')), $age_dir)
2064
            ->select(['f_id AS famid', $sex_field, new Expression(DB::prefix('married.d_julianday2') . ' - ' . DB::prefix('birth.d_julianday1') . ' AS age'), 'i_id'])
2065
            ->take(1)
2066
            ->get()
2067
            ->first();
2068
2069
        if ($row === null) {
2070
            return I18N::translate('This information is not available.');
2071
        }
2072
2073
        $family = Registry::familyFactory()->make($row->famid, $this->tree);
2074
        $person = Registry::individualFactory()->make($row->i_id, $this->tree);
2075
2076
        switch ($show) {
2077
            default:
2078
            case 'full':
2079
                if ($family !== null && $family->canShow()) {
2080
                    $result = $family->formatList();
2081
                } else {
2082
                    $result = I18N::translate('This information is private and cannot be shown.');
2083
                }
2084
                break;
2085
2086
            case 'name':
2087
                $result = '<a href="' . e($family->url()) . '">' . $person->fullName() . '</a>';
2088
                break;
2089
2090
            case 'age':
2091
                $age = $row->age;
2092
2093
                if ($show_years) {
2094
                    $result = $this->calculateAge((int) $row->age);
2095
                } else {
2096
                    $result = I18N::number((int) ($age / 365.25));
2097
                }
2098
2099
                break;
2100
        }
2101
2102
        return $result;
2103
    }
2104
2105
    /**
2106
     * Who is currently logged in?
2107
     *
2108
     * @param string $type "list" or "nolist"
2109
     */
2110
    private function usersLoggedInQuery(string $type): string
2111
    {
2112
        $content   = '';
2113
        $anonymous = 0;
2114
        $logged_in = [];
2115
2116
        foreach ($this->user_service->allLoggedIn() as $user) {
2117
            if (Auth::isAdmin() || $user->getPreference(UserInterface::PREF_IS_VISIBLE_ONLINE) === '1') {
2118
                $logged_in[] = $user;
2119
            } else {
2120
                $anonymous++;
2121
            }
2122
        }
2123
2124
        $count_logged_in = count($logged_in);
2125
2126
        if ($count_logged_in === 0 && $anonymous === 0) {
2127
            $content .= I18N::translate('No signed-in and no anonymous users');
2128
        }
2129
2130
        if ($anonymous > 0) {
2131
            $content .= '<b>' . I18N::plural('%s anonymous signed-in user', '%s anonymous signed-in users', $anonymous, I18N::number($anonymous)) . '</b>';
2132
        }
2133
2134
        if ($count_logged_in > 0) {
2135
            if ($anonymous !== 0) {
2136
                if ($type === 'list') {
2137
                    $content .= '<br><br>';
2138
                } else {
2139
                    $content .= ' ' . I18N::translate('and') . ' ';
2140
                }
2141
            }
2142
            $content .= '<b>' . I18N::plural('%s signed-in user', '%s signed-in users', $count_logged_in, I18N::number($count_logged_in)) . '</b>';
2143
            if ($type === 'list') {
2144
                $content .= '<ul>';
2145
            } else {
2146
                $content .= ': ';
2147
            }
2148
        }
2149
2150
        if (Auth::check()) {
2151
            foreach ($logged_in as $user) {
2152
                if ($type === 'list') {
2153
                    $content .= '<li>';
2154
                }
2155
2156
                $individual = Registry::individualFactory()->make($this->tree->getUserPreference($user, UserInterface::PREF_TREE_ACCOUNT_XREF), $this->tree);
2157
2158
                if ($individual instanceof Individual && $individual->canShow()) {
2159
                    $content .= '<a href="' . e($individual->url()) . '">' . e($user->realName()) . '</a>';
2160
                } else {
2161
                    $content .= e($user->realName());
2162
                }
2163
2164
                $content .= ' - ' . e($user->userName());
2165
2166
                if ($user->getPreference(UserInterface::PREF_CONTACT_METHOD) !== MessageService::CONTACT_METHOD_NONE && Auth::id() !== $user->id()) {
2167
                    $content .= '<a href="' . e(route(MessagePage::class, ['to' => $user->userName(), 'tree' => $this->tree->name()])) . '" class="btn btn-link" title="' . I18N::translate('Send a message') . '">' . view('icons/email') . '</a>';
2168
                }
2169
2170
                if ($type === 'list') {
2171
                    $content .= '</li>';
2172
                }
2173
            }
2174
        }
2175
2176
        if ($type === 'list') {
2177
            $content .= '</ul>';
2178
        }
2179
2180
        return $content;
2181
    }
2182
2183
    public function usersLoggedIn(): string
2184
    {
2185
        return $this->usersLoggedInQuery('nolist');
2186
    }
2187
2188
    public function usersLoggedInList(): string
2189
    {
2190
        return $this->usersLoggedInQuery('list');
2191
    }
2192
2193
    /**
2194
     * Century name, English => 21st, Polish => XXI, etc.
2195
     */
2196
    private function centuryName(int $century): string
2197
    {
2198
        if ($century < 0) {
2199
            return I18N::translate('%s BCE', $this->centuryName(-$century));
2200
        }
2201
2202
        // The current chart engine (Google charts) can't handle <sup></sup> markup
2203
        switch ($century) {
2204
            case 21:
2205
                return strip_tags(I18N::translateContext('CENTURY', '21st'));
2206
            case 20:
2207
                return strip_tags(I18N::translateContext('CENTURY', '20th'));
2208
            case 19:
2209
                return strip_tags(I18N::translateContext('CENTURY', '19th'));
2210
            case 18:
2211
                return strip_tags(I18N::translateContext('CENTURY', '18th'));
2212
            case 17:
2213
                return strip_tags(I18N::translateContext('CENTURY', '17th'));
2214
            case 16:
2215
                return strip_tags(I18N::translateContext('CENTURY', '16th'));
2216
            case 15:
2217
                return strip_tags(I18N::translateContext('CENTURY', '15th'));
2218
            case 14:
2219
                return strip_tags(I18N::translateContext('CENTURY', '14th'));
2220
            case 13:
2221
                return strip_tags(I18N::translateContext('CENTURY', '13th'));
2222
            case 12:
2223
                return strip_tags(I18N::translateContext('CENTURY', '12th'));
2224
            case 11:
2225
                return strip_tags(I18N::translateContext('CENTURY', '11th'));
2226
            case 10:
2227
                return strip_tags(I18N::translateContext('CENTURY', '10th'));
2228
            case 9:
2229
                return strip_tags(I18N::translateContext('CENTURY', '9th'));
2230
            case 8:
2231
                return strip_tags(I18N::translateContext('CENTURY', '8th'));
2232
            case 7:
2233
                return strip_tags(I18N::translateContext('CENTURY', '7th'));
2234
            case 6:
2235
                return strip_tags(I18N::translateContext('CENTURY', '6th'));
2236
            case 5:
2237
                return strip_tags(I18N::translateContext('CENTURY', '5th'));
2238
            case 4:
2239
                return strip_tags(I18N::translateContext('CENTURY', '4th'));
2240
            case 3:
2241
                return strip_tags(I18N::translateContext('CENTURY', '3rd'));
2242
            case 2:
2243
                return strip_tags(I18N::translateContext('CENTURY', '2nd'));
2244
            case 1:
2245
                return strip_tags(I18N::translateContext('CENTURY', '1st'));
2246
            default:
2247
                return ($century - 1) . '01-' . $century . '00';
2248
        }
2249
    }
2250
2251
    /**
2252
     * @return array<string>
2253
     */
2254
    private function getAllCountries(): array
2255
    {
2256
        return [
2257
            /* I18N: Name of a country or state */
2258
            '???' => I18N::translate('Unknown'),
2259
            /* I18N: Name of a country or state */
2260
            'ABW' => I18N::translate('Aruba'),
2261
            /* I18N: Name of a country or state */
2262
            'AFG' => I18N::translate('Afghanistan'),
2263
            /* I18N: Name of a country or state */
2264
            'AGO' => I18N::translate('Angola'),
2265
            /* I18N: Name of a country or state */
2266
            'AIA' => I18N::translate('Anguilla'),
2267
            /* I18N: Name of a country or state */
2268
            'ALA' => I18N::translate('Åland Islands'),
2269
            /* I18N: Name of a country or state */
2270
            'ALB' => I18N::translate('Albania'),
2271
            /* I18N: Name of a country or state */
2272
            'AND' => I18N::translate('Andorra'),
2273
            /* I18N: Name of a country or state */
2274
            'ARE' => I18N::translate('United Arab Emirates'),
2275
            /* I18N: Name of a country or state */
2276
            'ARG' => I18N::translate('Argentina'),
2277
            /* I18N: Name of a country or state */
2278
            'ARM' => I18N::translate('Armenia'),
2279
            /* I18N: Name of a country or state */
2280
            'ASM' => I18N::translate('American Samoa'),
2281
            /* I18N: Name of a country or state */
2282
            'ATA' => I18N::translate('Antarctica'),
2283
            /* I18N: Name of a country or state */
2284
            'ATF' => I18N::translate('French Southern Territories'),
2285
            /* I18N: Name of a country or state */
2286
            'ATG' => I18N::translate('Antigua and Barbuda'),
2287
            /* I18N: Name of a country or state */
2288
            'AUS' => I18N::translate('Australia'),
2289
            /* I18N: Name of a country or state */
2290
            'AUT' => I18N::translate('Austria'),
2291
            /* I18N: Name of a country or state */
2292
            'AZE' => I18N::translate('Azerbaijan'),
2293
            /* I18N: Name of a country or state */
2294
            'AZR' => I18N::translate('Azores'),
2295
            /* I18N: Name of a country or state */
2296
            'BDI' => I18N::translate('Burundi'),
2297
            /* I18N: Name of a country or state */
2298
            'BEL' => I18N::translate('Belgium'),
2299
            /* I18N: Name of a country or state */
2300
            'BEN' => I18N::translate('Benin'),
2301
            // BES => Bonaire, Sint Eustatius and Saba
2302
            /* I18N: Name of a country or state */
2303
            'BFA' => I18N::translate('Burkina Faso'),
2304
            /* I18N: Name of a country or state */
2305
            'BGD' => I18N::translate('Bangladesh'),
2306
            /* I18N: Name of a country or state */
2307
            'BGR' => I18N::translate('Bulgaria'),
2308
            /* I18N: Name of a country or state */
2309
            'BHR' => I18N::translate('Bahrain'),
2310
            /* I18N: Name of a country or state */
2311
            'BHS' => I18N::translate('Bahamas'),
2312
            /* I18N: Name of a country or state */
2313
            'BIH' => I18N::translate('Bosnia and Herzegovina'),
2314
            // BLM => Saint Barthélemy
2315
            'BLM' => I18N::translate('Saint Barthélemy'),
2316
            /* I18N: Name of a country or state */
2317
            'BLR' => I18N::translate('Belarus'),
2318
            /* I18N: Name of a country or state */
2319
            'BLZ' => I18N::translate('Belize'),
2320
            /* I18N: Name of a country or state */
2321
            'BMU' => I18N::translate('Bermuda'),
2322
            /* I18N: Name of a country or state */
2323
            'BOL' => I18N::translate('Bolivia'),
2324
            /* I18N: Name of a country or state */
2325
            'BRA' => I18N::translate('Brazil'),
2326
            /* I18N: Name of a country or state */
2327
            'BRB' => I18N::translate('Barbados'),
2328
            /* I18N: Name of a country or state */
2329
            'BRN' => I18N::translate('Brunei Darussalam'),
2330
            /* I18N: Name of a country or state */
2331
            'BTN' => I18N::translate('Bhutan'),
2332
            /* I18N: Name of a country or state */
2333
            'BVT' => I18N::translate('Bouvet Island'),
2334
            /* I18N: Name of a country or state */
2335
            'BWA' => I18N::translate('Botswana'),
2336
            /* I18N: Name of a country or state */
2337
            'CAF' => I18N::translate('Central African Republic'),
2338
            /* I18N: Name of a country or state */
2339
            'CAN' => I18N::translate('Canada'),
2340
            /* I18N: Name of a country or state */
2341
            'CCK' => I18N::translate('Cocos (Keeling) Islands'),
2342
            /* I18N: Name of a country or state */
2343
            'CHE' => I18N::translate('Switzerland'),
2344
            /* I18N: Name of a country or state */
2345
            'CHL' => I18N::translate('Chile'),
2346
            /* I18N: Name of a country or state */
2347
            'CHN' => I18N::translate('China'),
2348
            /* I18N: Name of a country or state */
2349
            'CIV' => I18N::translate('Côte d’Ivoire'),
2350
            /* I18N: Name of a country or state */
2351
            'CMR' => I18N::translate('Cameroon'),
2352
            /* I18N: Name of a country or state */
2353
            'COD' => I18N::translate('Democratic Republic of the Congo'),
2354
            /* I18N: Name of a country or state */
2355
            'COG' => I18N::translate('Republic of the Congo'),
2356
            /* I18N: Name of a country or state */
2357
            'COK' => I18N::translate('Cook Islands'),
2358
            /* I18N: Name of a country or state */
2359
            'COL' => I18N::translate('Colombia'),
2360
            /* I18N: Name of a country or state */
2361
            'COM' => I18N::translate('Comoros'),
2362
            /* I18N: Name of a country or state */
2363
            'CPV' => I18N::translate('Cape Verde'),
2364
            /* I18N: Name of a country or state */
2365
            'CRI' => I18N::translate('Costa Rica'),
2366
            /* I18N: Name of a country or state */
2367
            'CUB' => I18N::translate('Cuba'),
2368
            /* I18N: Name of a country or state */
2369
            'CUW' => I18N::translate('Curaçao'),
2370
            /* I18N: Name of a country or state */
2371
            'CXR' => I18N::translate('Christmas Island'),
2372
            /* I18N: Name of a country or state */
2373
            'CYM' => I18N::translate('Cayman Islands'),
2374
            /* I18N: Name of a country or state */
2375
            'CYP' => I18N::translate('Cyprus'),
2376
            /* I18N: Name of a country or state */
2377
            'CZE' => I18N::translate('Czech Republic'),
2378
            /* I18N: Name of a country or state */
2379
            'DEU' => I18N::translate('Germany'),
2380
            /* I18N: Name of a country or state */
2381
            'DJI' => I18N::translate('Djibouti'),
2382
            /* I18N: Name of a country or state */
2383
            'DMA' => I18N::translate('Dominica'),
2384
            /* I18N: Name of a country or state */
2385
            'DNK' => I18N::translate('Denmark'),
2386
            /* I18N: Name of a country or state */
2387
            'DOM' => I18N::translate('Dominican Republic'),
2388
            /* I18N: Name of a country or state */
2389
            'DZA' => I18N::translate('Algeria'),
2390
            /* I18N: Name of a country or state */
2391
            'ECU' => I18N::translate('Ecuador'),
2392
            /* I18N: Name of a country or state */
2393
            'EGY' => I18N::translate('Egypt'),
2394
            /* I18N: Name of a country or state */
2395
            'ENG' => I18N::translate('England'),
2396
            /* I18N: Name of a country or state */
2397
            'ERI' => I18N::translate('Eritrea'),
2398
            /* I18N: Name of a country or state */
2399
            'ESH' => I18N::translate('Western Sahara'),
2400
            /* I18N: Name of a country or state */
2401
            'ESP' => I18N::translate('Spain'),
2402
            /* I18N: Name of a country or state */
2403
            'EST' => I18N::translate('Estonia'),
2404
            /* I18N: Name of a country or state */
2405
            'ETH' => I18N::translate('Ethiopia'),
2406
            /* I18N: Name of a country or state */
2407
            'FIN' => I18N::translate('Finland'),
2408
            /* I18N: Name of a country or state */
2409
            'FJI' => I18N::translate('Fiji'),
2410
            /* I18N: Name of a country or state */
2411
            'FLD' => I18N::translate('Flanders'),
2412
            /* I18N: Name of a country or state */
2413
            'FLK' => I18N::translate('Falkland Islands'),
2414
            /* I18N: Name of a country or state */
2415
            'FRA' => I18N::translate('France'),
2416
            /* I18N: Name of a country or state */
2417
            'FRO' => I18N::translate('Faroe Islands'),
2418
            /* I18N: Name of a country or state */
2419
            'FSM' => I18N::translate('Micronesia'),
2420
            /* I18N: Name of a country or state */
2421
            'GAB' => I18N::translate('Gabon'),
2422
            /* I18N: Name of a country or state */
2423
            'GBR' => I18N::translate('United Kingdom'),
2424
            /* I18N: Name of a country or state */
2425
            'GEO' => I18N::translate('Georgia'),
2426
            /* I18N: Name of a country or state */
2427
            'GGY' => I18N::translate('Guernsey'),
2428
            /* I18N: Name of a country or state */
2429
            'GHA' => I18N::translate('Ghana'),
2430
            /* I18N: Name of a country or state */
2431
            'GIB' => I18N::translate('Gibraltar'),
2432
            /* I18N: Name of a country or state */
2433
            'GIN' => I18N::translate('Guinea'),
2434
            /* I18N: Name of a country or state */
2435
            'GLP' => I18N::translate('Guadeloupe'),
2436
            /* I18N: Name of a country or state */
2437
            'GMB' => I18N::translate('Gambia'),
2438
            /* I18N: Name of a country or state */
2439
            'GNB' => I18N::translate('Guinea-Bissau'),
2440
            /* I18N: Name of a country or state */
2441
            'GNQ' => I18N::translate('Equatorial Guinea'),
2442
            /* I18N: Name of a country or state */
2443
            'GRC' => I18N::translate('Greece'),
2444
            /* I18N: Name of a country or state */
2445
            'GRD' => I18N::translate('Grenada'),
2446
            /* I18N: Name of a country or state */
2447
            'GRL' => I18N::translate('Greenland'),
2448
            /* I18N: Name of a country or state */
2449
            'GTM' => I18N::translate('Guatemala'),
2450
            /* I18N: Name of a country or state */
2451
            'GUF' => I18N::translate('French Guiana'),
2452
            /* I18N: Name of a country or state */
2453
            'GUM' => I18N::translate('Guam'),
2454
            /* I18N: Name of a country or state */
2455
            'GUY' => I18N::translate('Guyana'),
2456
            /* I18N: Name of a country or state */
2457
            'HKG' => I18N::translate('Hong Kong'),
2458
            /* I18N: Name of a country or state */
2459
            'HMD' => I18N::translate('Heard Island and McDonald Islands'),
2460
            /* I18N: Name of a country or state */
2461
            'HND' => I18N::translate('Honduras'),
2462
            /* I18N: Name of a country or state */
2463
            'HRV' => I18N::translate('Croatia'),
2464
            /* I18N: Name of a country or state */
2465
            'HTI' => I18N::translate('Haiti'),
2466
            /* I18N: Name of a country or state */
2467
            'HUN' => I18N::translate('Hungary'),
2468
            /* I18N: Name of a country or state */
2469
            'IDN' => I18N::translate('Indonesia'),
2470
            /* I18N: Name of a country or state */
2471
            'IND' => I18N::translate('India'),
2472
            /* I18N: Name of a country or state */
2473
            'IOM' => I18N::translate('Isle of Man'),
2474
            /* I18N: Name of a country or state */
2475
            'IOT' => I18N::translate('British Indian Ocean Territory'),
2476
            /* I18N: Name of a country or state */
2477
            'IRL' => I18N::translate('Ireland'),
2478
            /* I18N: Name of a country or state */
2479
            'IRN' => I18N::translate('Iran'),
2480
            /* I18N: Name of a country or state */
2481
            'IRQ' => I18N::translate('Iraq'),
2482
            /* I18N: Name of a country or state */
2483
            'ISL' => I18N::translate('Iceland'),
2484
            /* I18N: Name of a country or state */
2485
            'ISR' => I18N::translate('Israel'),
2486
            /* I18N: Name of a country or state */
2487
            'ITA' => I18N::translate('Italy'),
2488
            /* I18N: Name of a country or state */
2489
            'JAM' => I18N::translate('Jamaica'),
2490
            //'JEY' => Jersey
2491
            /* I18N: Name of a country or state */
2492
            'JOR' => I18N::translate('Jordan'),
2493
            /* I18N: Name of a country or state */
2494
            'JPN' => I18N::translate('Japan'),
2495
            /* I18N: Name of a country or state */
2496
            'KAZ' => I18N::translate('Kazakhstan'),
2497
            /* I18N: Name of a country or state */
2498
            'KEN' => I18N::translate('Kenya'),
2499
            /* I18N: Name of a country or state */
2500
            'KGZ' => I18N::translate('Kyrgyzstan'),
2501
            /* I18N: Name of a country or state */
2502
            'KHM' => I18N::translate('Cambodia'),
2503
            /* I18N: Name of a country or state */
2504
            'KIR' => I18N::translate('Kiribati'),
2505
            /* I18N: Name of a country or state */
2506
            'KNA' => I18N::translate('Saint Kitts and Nevis'),
2507
            /* I18N: Name of a country or state */
2508
            'KOR' => I18N::translate('Korea'),
2509
            /* I18N: Name of a country or state */
2510
            'KWT' => I18N::translate('Kuwait'),
2511
            /* I18N: Name of a country or state */
2512
            'LAO' => I18N::translate('Laos'),
2513
            /* I18N: Name of a country or state */
2514
            'LBN' => I18N::translate('Lebanon'),
2515
            /* I18N: Name of a country or state */
2516
            'LBR' => I18N::translate('Liberia'),
2517
            /* I18N: Name of a country or state */
2518
            'LBY' => I18N::translate('Libya'),
2519
            /* I18N: Name of a country or state */
2520
            'LCA' => I18N::translate('Saint Lucia'),
2521
            /* I18N: Name of a country or state */
2522
            'LIE' => I18N::translate('Liechtenstein'),
2523
            /* I18N: Name of a country or state */
2524
            'LKA' => I18N::translate('Sri Lanka'),
2525
            /* I18N: Name of a country or state */
2526
            'LSO' => I18N::translate('Lesotho'),
2527
            /* I18N: Name of a country or state */
2528
            'LTU' => I18N::translate('Lithuania'),
2529
            /* I18N: Name of a country or state */
2530
            'LUX' => I18N::translate('Luxembourg'),
2531
            /* I18N: Name of a country or state */
2532
            'LVA' => I18N::translate('Latvia'),
2533
            /* I18N: Name of a country or state */
2534
            'MAC' => I18N::translate('Macau'),
2535
            // MAF => Saint Martin
2536
            /* I18N: Name of a country or state */
2537
            'MAR' => I18N::translate('Morocco'),
2538
            /* I18N: Name of a country or state */
2539
            'MCO' => I18N::translate('Monaco'),
2540
            /* I18N: Name of a country or state */
2541
            'MDA' => I18N::translate('Moldova'),
2542
            /* I18N: Name of a country or state */
2543
            'MDG' => I18N::translate('Madagascar'),
2544
            /* I18N: Name of a country or state */
2545
            'MDV' => I18N::translate('Maldives'),
2546
            /* I18N: Name of a country or state */
2547
            'MEX' => I18N::translate('Mexico'),
2548
            /* I18N: Name of a country or state */
2549
            'MHL' => I18N::translate('Marshall Islands'),
2550
            /* I18N: Name of a country or state */
2551
            'MKD' => I18N::translate('Macedonia'),
2552
            /* I18N: Name of a country or state */
2553
            'MLI' => I18N::translate('Mali'),
2554
            /* I18N: Name of a country or state */
2555
            'MLT' => I18N::translate('Malta'),
2556
            /* I18N: Name of a country or state */
2557
            'MMR' => I18N::translate('Myanmar'),
2558
            /* I18N: Name of a country or state */
2559
            'MNG' => I18N::translate('Mongolia'),
2560
            /* I18N: Name of a country or state */
2561
            'MNP' => I18N::translate('Northern Mariana Islands'),
2562
            /* I18N: Name of a country or state */
2563
            'MNT' => I18N::translate('Montenegro'),
2564
            /* I18N: Name of a country or state */
2565
            'MOZ' => I18N::translate('Mozambique'),
2566
            /* I18N: Name of a country or state */
2567
            'MRT' => I18N::translate('Mauritania'),
2568
            /* I18N: Name of a country or state */
2569
            'MSR' => I18N::translate('Montserrat'),
2570
            /* I18N: Name of a country or state */
2571
            'MTQ' => I18N::translate('Martinique'),
2572
            /* I18N: Name of a country or state */
2573
            'MUS' => I18N::translate('Mauritius'),
2574
            /* I18N: Name of a country or state */
2575
            'MWI' => I18N::translate('Malawi'),
2576
            /* I18N: Name of a country or state */
2577
            'MYS' => I18N::translate('Malaysia'),
2578
            /* I18N: Name of a country or state */
2579
            'MYT' => I18N::translate('Mayotte'),
2580
            /* I18N: Name of a country or state */
2581
            'NAM' => I18N::translate('Namibia'),
2582
            /* I18N: Name of a country or state */
2583
            'NCL' => I18N::translate('New Caledonia'),
2584
            /* I18N: Name of a country or state */
2585
            'NER' => I18N::translate('Niger'),
2586
            /* I18N: Name of a country or state */
2587
            'NFK' => I18N::translate('Norfolk Island'),
2588
            /* I18N: Name of a country or state */
2589
            'NGA' => I18N::translate('Nigeria'),
2590
            /* I18N: Name of a country or state */
2591
            'NIC' => I18N::translate('Nicaragua'),
2592
            /* I18N: Name of a country or state */
2593
            'NIR' => I18N::translate('Northern Ireland'),
2594
            /* I18N: Name of a country or state */
2595
            'NIU' => I18N::translate('Niue'),
2596
            /* I18N: Name of a country or state */
2597
            'NLD' => I18N::translate('Netherlands'),
2598
            /* I18N: Name of a country or state */
2599
            'NOR' => I18N::translate('Norway'),
2600
            /* I18N: Name of a country or state */
2601
            'NPL' => I18N::translate('Nepal'),
2602
            /* I18N: Name of a country or state */
2603
            'NRU' => I18N::translate('Nauru'),
2604
            /* I18N: Name of a country or state */
2605
            'NZL' => I18N::translate('New Zealand'),
2606
            /* I18N: Name of a country or state */
2607
            'OMN' => I18N::translate('Oman'),
2608
            /* I18N: Name of a country or state */
2609
            'PAK' => I18N::translate('Pakistan'),
2610
            /* I18N: Name of a country or state */
2611
            'PAN' => I18N::translate('Panama'),
2612
            /* I18N: Name of a country or state */
2613
            'PCN' => I18N::translate('Pitcairn'),
2614
            /* I18N: Name of a country or state */
2615
            'PER' => I18N::translate('Peru'),
2616
            /* I18N: Name of a country or state */
2617
            'PHL' => I18N::translate('Philippines'),
2618
            /* I18N: Name of a country or state */
2619
            'PLW' => I18N::translate('Palau'),
2620
            /* I18N: Name of a country or state */
2621
            'PNG' => I18N::translate('Papua New Guinea'),
2622
            /* I18N: Name of a country or state */
2623
            'POL' => I18N::translate('Poland'),
2624
            /* I18N: Name of a country or state */
2625
            'PRI' => I18N::translate('Puerto Rico'),
2626
            /* I18N: Name of a country or state */
2627
            'PRK' => I18N::translate('North Korea'),
2628
            /* I18N: Name of a country or state */
2629
            'PRT' => I18N::translate('Portugal'),
2630
            /* I18N: Name of a country or state */
2631
            'PRY' => I18N::translate('Paraguay'),
2632
            /* I18N: Name of a country or state */
2633
            'PSE' => I18N::translate('Occupied Palestinian Territory'),
2634
            /* I18N: Name of a country or state */
2635
            'PYF' => I18N::translate('French Polynesia'),
2636
            /* I18N: Name of a country or state */
2637
            'QAT' => I18N::translate('Qatar'),
2638
            /* I18N: Name of a country or state */
2639
            'REU' => I18N::translate('Réunion'),
2640
            /* I18N: Name of a country or state */
2641
            'ROM' => I18N::translate('Romania'),
2642
            /* I18N: Name of a country or state */
2643
            'RUS' => I18N::translate('Russia'),
2644
            /* I18N: Name of a country or state */
2645
            'RWA' => I18N::translate('Rwanda'),
2646
            /* I18N: Name of a country or state */
2647
            'SAU' => I18N::translate('Saudi Arabia'),
2648
            /* I18N: Name of a country or state */
2649
            'SCT' => I18N::translate('Scotland'),
2650
            /* I18N: Name of a country or state */
2651
            'SDN' => I18N::translate('Sudan'),
2652
            /* I18N: Name of a country or state */
2653
            'SEA' => I18N::translate('At sea'),
2654
            /* I18N: Name of a country or state */
2655
            'SEN' => I18N::translate('Senegal'),
2656
            /* I18N: Name of a country or state */
2657
            'SER' => I18N::translate('Serbia'),
2658
            /* I18N: Name of a country or state */
2659
            'SGP' => I18N::translate('Singapore'),
2660
            /* I18N: Name of a country or state */
2661
            'SGS' => I18N::translate('South Georgia and the South Sandwich Islands'),
2662
            /* I18N: Name of a country or state */
2663
            'SHN' => I18N::translate('Saint Helena'),
2664
            /* I18N: Name of a country or state */
2665
            'SJM' => I18N::translate('Svalbard and Jan Mayen'),
2666
            /* I18N: Name of a country or state */
2667
            'SLB' => I18N::translate('Solomon Islands'),
2668
            /* I18N: Name of a country or state */
2669
            'SLE' => I18N::translate('Sierra Leone'),
2670
            /* I18N: Name of a country or state */
2671
            'SLV' => I18N::translate('El Salvador'),
2672
            /* I18N: Name of a country or state */
2673
            'SMR' => I18N::translate('San Marino'),
2674
            /* I18N: Name of a country or state */
2675
            'SOM' => I18N::translate('Somalia'),
2676
            /* I18N: Name of a country or state */
2677
            'SPM' => I18N::translate('Saint Pierre and Miquelon'),
2678
            /* I18N: Name of a country or state */
2679
            'SSD' => I18N::translate('South Sudan'),
2680
            /* I18N: Name of a country or state */
2681
            'STP' => I18N::translate('Sao Tome and Principe'),
2682
            /* I18N: Name of a country or state */
2683
            'SUR' => I18N::translate('Suriname'),
2684
            /* I18N: Name of a country or state */
2685
            'SVK' => I18N::translate('Slovakia'),
2686
            /* I18N: Name of a country or state */
2687
            'SVN' => I18N::translate('Slovenia'),
2688
            /* I18N: Name of a country or state */
2689
            'SWE' => I18N::translate('Sweden'),
2690
            /* I18N: Name of a country or state */
2691
            'SWZ' => I18N::translate('Swaziland'),
2692
            // SXM => Sint Maarten
2693
            /* I18N: Name of a country or state */
2694
            'SYC' => I18N::translate('Seychelles'),
2695
            /* I18N: Name of a country or state */
2696
            'SYR' => I18N::translate('Syria'),
2697
            /* I18N: Name of a country or state */
2698
            'TCA' => I18N::translate('Turks and Caicos Islands'),
2699
            /* I18N: Name of a country or state */
2700
            'TCD' => I18N::translate('Chad'),
2701
            /* I18N: Name of a country or state */
2702
            'TGO' => I18N::translate('Togo'),
2703
            /* I18N: Name of a country or state */
2704
            'THA' => I18N::translate('Thailand'),
2705
            /* I18N: Name of a country or state */
2706
            'TJK' => I18N::translate('Tajikistan'),
2707
            /* I18N: Name of a country or state */
2708
            'TKL' => I18N::translate('Tokelau'),
2709
            /* I18N: Name of a country or state */
2710
            'TKM' => I18N::translate('Turkmenistan'),
2711
            /* I18N: Name of a country or state */
2712
            'TLS' => I18N::translate('Timor-Leste'),
2713
            /* I18N: Name of a country or state */
2714
            'TON' => I18N::translate('Tonga'),
2715
            /* I18N: Name of a country or state */
2716
            'TTO' => I18N::translate('Trinidad and Tobago'),
2717
            /* I18N: Name of a country or state */
2718
            'TUN' => I18N::translate('Tunisia'),
2719
            /* I18N: Name of a country or state */
2720
            'TUR' => I18N::translate('Turkey'),
2721
            /* I18N: Name of a country or state */
2722
            'TUV' => I18N::translate('Tuvalu'),
2723
            /* I18N: Name of a country or state */
2724
            'TWN' => I18N::translate('Taiwan'),
2725
            /* I18N: Name of a country or state */
2726
            'TZA' => I18N::translate('Tanzania'),
2727
            /* I18N: Name of a country or state */
2728
            'UGA' => I18N::translate('Uganda'),
2729
            /* I18N: Name of a country or state */
2730
            'UKR' => I18N::translate('Ukraine'),
2731
            /* I18N: Name of a country or state */
2732
            'UMI' => I18N::translate('US Minor Outlying Islands'),
2733
            /* I18N: Name of a country or state */
2734
            'URY' => I18N::translate('Uruguay'),
2735
            /* I18N: Name of a country or state */
2736
            'USA' => I18N::translate('United States'),
2737
            /* I18N: Name of a country or state */
2738
            'UZB' => I18N::translate('Uzbekistan'),
2739
            /* I18N: Name of a country or state */
2740
            'VAT' => I18N::translate('Vatican City'),
2741
            /* I18N: Name of a country or state */
2742
            'VCT' => I18N::translate('Saint Vincent and the Grenadines'),
2743
            /* I18N: Name of a country or state */
2744
            'VEN' => I18N::translate('Venezuela'),
2745
            /* I18N: Name of a country or state */
2746
            'VGB' => I18N::translate('British Virgin Islands'),
2747
            /* I18N: Name of a country or state */
2748
            'VIR' => I18N::translate('US Virgin Islands'),
2749
            /* I18N: Name of a country or state */
2750
            'VNM' => I18N::translate('Vietnam'),
2751
            /* I18N: Name of a country or state */
2752
            'VUT' => I18N::translate('Vanuatu'),
2753
            /* I18N: Name of a country or state */
2754
            'WLF' => I18N::translate('Wallis and Futuna'),
2755
            /* I18N: Name of a country or state */
2756
            'WLS' => I18N::translate('Wales'),
2757
            /* I18N: Name of a country or state */
2758
            'WSM' => I18N::translate('Samoa'),
2759
            /* I18N: Name of a country or state */
2760
            'YEM' => I18N::translate('Yemen'),
2761
            /* I18N: Name of a country or state */
2762
            'ZAF' => I18N::translate('South Africa'),
2763
            /* I18N: Name of a country or state */
2764
            'ZMB' => I18N::translate('Zambia'),
2765
            /* I18N: Name of a country or state */
2766
            'ZWE' => I18N::translate('Zimbabwe'),
2767
        ];
2768
    }
2769
2770
    /**
2771
     * ISO3166 3 letter codes, with their 2 letter equivalent.
2772
     * NOTE: this is not 1:1. ENG/SCO/WAL/NIR => GB
2773
     * NOTE: this also includes chapman codes and others. Should it?
2774
     *
2775
     * @return array<string>
2776
     */
2777
    private function iso3166(): array
2778
    {
2779
        return [
2780
            'GBR' => 'GB', // Must come before ENG, NIR, SCT and WLS
2781
            'ABW' => 'AW',
2782
            'AFG' => 'AF',
2783
            'AGO' => 'AO',
2784
            'AIA' => 'AI',
2785
            'ALA' => 'AX',
2786
            'ALB' => 'AL',
2787
            'AND' => 'AD',
2788
            'ARE' => 'AE',
2789
            'ARG' => 'AR',
2790
            'ARM' => 'AM',
2791
            'ASM' => 'AS',
2792
            'ATA' => 'AQ',
2793
            'ATF' => 'TF',
2794
            'ATG' => 'AG',
2795
            'AUS' => 'AU',
2796
            'AUT' => 'AT',
2797
            'AZE' => 'AZ',
2798
            'BDI' => 'BI',
2799
            'BEL' => 'BE',
2800
            'BEN' => 'BJ',
2801
            'BFA' => 'BF',
2802
            'BGD' => 'BD',
2803
            'BGR' => 'BG',
2804
            'BHR' => 'BH',
2805
            'BHS' => 'BS',
2806
            'BIH' => 'BA',
2807
            'BLR' => 'BY',
2808
            'BLZ' => 'BZ',
2809
            'BMU' => 'BM',
2810
            'BOL' => 'BO',
2811
            'BRA' => 'BR',
2812
            'BRB' => 'BB',
2813
            'BRN' => 'BN',
2814
            'BTN' => 'BT',
2815
            'BVT' => 'BV',
2816
            'BWA' => 'BW',
2817
            'CAF' => 'CF',
2818
            'CAN' => 'CA',
2819
            'CCK' => 'CC',
2820
            'CHE' => 'CH',
2821
            'CHL' => 'CL',
2822
            'CHN' => 'CN',
2823
            'CIV' => 'CI',
2824
            'CMR' => 'CM',
2825
            'COD' => 'CD',
2826
            'COG' => 'CG',
2827
            'COK' => 'CK',
2828
            'COL' => 'CO',
2829
            'COM' => 'KM',
2830
            'CPV' => 'CV',
2831
            'CRI' => 'CR',
2832
            'CUB' => 'CU',
2833
            'CXR' => 'CX',
2834
            'CYM' => 'KY',
2835
            'CYP' => 'CY',
2836
            'CZE' => 'CZ',
2837
            'DEU' => 'DE',
2838
            'DJI' => 'DJ',
2839
            'DMA' => 'DM',
2840
            'DNK' => 'DK',
2841
            'DOM' => 'DO',
2842
            'DZA' => 'DZ',
2843
            'ECU' => 'EC',
2844
            'EGY' => 'EG',
2845
            'ENG' => 'GB',
2846
            'ERI' => 'ER',
2847
            'ESH' => 'EH',
2848
            'ESP' => 'ES',
2849
            'EST' => 'EE',
2850
            'ETH' => 'ET',
2851
            'FIN' => 'FI',
2852
            'FJI' => 'FJ',
2853
            'FLK' => 'FK',
2854
            'FRA' => 'FR',
2855
            'FRO' => 'FO',
2856
            'FSM' => 'FM',
2857
            'GAB' => 'GA',
2858
            'GEO' => 'GE',
2859
            'GHA' => 'GH',
2860
            'GIB' => 'GI',
2861
            'GIN' => 'GN',
2862
            'GLP' => 'GP',
2863
            'GMB' => 'GM',
2864
            'GNB' => 'GW',
2865
            'GNQ' => 'GQ',
2866
            'GRC' => 'GR',
2867
            'GRD' => 'GD',
2868
            'GRL' => 'GL',
2869
            'GTM' => 'GT',
2870
            'GUF' => 'GF',
2871
            'GUM' => 'GU',
2872
            'GUY' => 'GY',
2873
            'HKG' => 'HK',
2874
            'HMD' => 'HM',
2875
            'HND' => 'HN',
2876
            'HRV' => 'HR',
2877
            'HTI' => 'HT',
2878
            'HUN' => 'HU',
2879
            'IDN' => 'ID',
2880
            'IND' => 'IN',
2881
            'IOT' => 'IO',
2882
            'IRL' => 'IE',
2883
            'IRN' => 'IR',
2884
            'IRQ' => 'IQ',
2885
            'ISL' => 'IS',
2886
            'ISR' => 'IL',
2887
            'ITA' => 'IT',
2888
            'JAM' => 'JM',
2889
            'JOR' => 'JO',
2890
            'JPN' => 'JP',
2891
            'KAZ' => 'KZ',
2892
            'KEN' => 'KE',
2893
            'KGZ' => 'KG',
2894
            'KHM' => 'KH',
2895
            'KIR' => 'KI',
2896
            'KNA' => 'KN',
2897
            'KOR' => 'KO',
2898
            'KWT' => 'KW',
2899
            'LAO' => 'LA',
2900
            'LBN' => 'LB',
2901
            'LBR' => 'LR',
2902
            'LBY' => 'LY',
2903
            'LCA' => 'LC',
2904
            'LIE' => 'LI',
2905
            'LKA' => 'LK',
2906
            'LSO' => 'LS',
2907
            'LTU' => 'LT',
2908
            'LUX' => 'LU',
2909
            'LVA' => 'LV',
2910
            'MAC' => 'MO',
2911
            'MAR' => 'MA',
2912
            'MCO' => 'MC',
2913
            'MDA' => 'MD',
2914
            'MDG' => 'MG',
2915
            'MDV' => 'MV',
2916
            'MEX' => 'MX',
2917
            'MHL' => 'MH',
2918
            'MKD' => 'MK',
2919
            'MLI' => 'ML',
2920
            'MLT' => 'MT',
2921
            'MMR' => 'MM',
2922
            'MNG' => 'MN',
2923
            'MNP' => 'MP',
2924
            'MNT' => 'ME',
2925
            'MOZ' => 'MZ',
2926
            'MRT' => 'MR',
2927
            'MSR' => 'MS',
2928
            'MTQ' => 'MQ',
2929
            'MUS' => 'MU',
2930
            'MWI' => 'MW',
2931
            'MYS' => 'MY',
2932
            'MYT' => 'YT',
2933
            'NAM' => 'NA',
2934
            'NCL' => 'NC',
2935
            'NER' => 'NE',
2936
            'NFK' => 'NF',
2937
            'NGA' => 'NG',
2938
            'NIC' => 'NI',
2939
            'NIR' => 'GB',
2940
            'NIU' => 'NU',
2941
            'NLD' => 'NL',
2942
            'NOR' => 'NO',
2943
            'NPL' => 'NP',
2944
            'NRU' => 'NR',
2945
            'NZL' => 'NZ',
2946
            'OMN' => 'OM',
2947
            'PAK' => 'PK',
2948
            'PAN' => 'PA',
2949
            'PCN' => 'PN',
2950
            'PER' => 'PE',
2951
            'PHL' => 'PH',
2952
            'PLW' => 'PW',
2953
            'PNG' => 'PG',
2954
            'POL' => 'PL',
2955
            'PRI' => 'PR',
2956
            'PRK' => 'KP',
2957
            'PRT' => 'PT',
2958
            'PRY' => 'PY',
2959
            'PSE' => 'PS',
2960
            'PYF' => 'PF',
2961
            'QAT' => 'QA',
2962
            'REU' => 'RE',
2963
            'ROM' => 'RO',
2964
            'RUS' => 'RU',
2965
            'RWA' => 'RW',
2966
            'SAU' => 'SA',
2967
            'SCT' => 'GB',
2968
            'SDN' => 'SD',
2969
            'SEN' => 'SN',
2970
            'SER' => 'RS',
2971
            'SGP' => 'SG',
2972
            'SGS' => 'GS',
2973
            'SHN' => 'SH',
2974
            'SJM' => 'SJ',
2975
            'SLB' => 'SB',
2976
            'SLE' => 'SL',
2977
            'SLV' => 'SV',
2978
            'SMR' => 'SM',
2979
            'SOM' => 'SO',
2980
            'SPM' => 'PM',
2981
            'STP' => 'ST',
2982
            'SUR' => 'SR',
2983
            'SVK' => 'SK',
2984
            'SVN' => 'SI',
2985
            'SWE' => 'SE',
2986
            'SWZ' => 'SZ',
2987
            'SYC' => 'SC',
2988
            'SYR' => 'SY',
2989
            'TCA' => 'TC',
2990
            'TCD' => 'TD',
2991
            'TGO' => 'TG',
2992
            'THA' => 'TH',
2993
            'TJK' => 'TJ',
2994
            'TKL' => 'TK',
2995
            'TKM' => 'TM',
2996
            'TLS' => 'TL',
2997
            'TON' => 'TO',
2998
            'TTO' => 'TT',
2999
            'TUN' => 'TN',
3000
            'TUR' => 'TR',
3001
            'TUV' => 'TV',
3002
            'TWN' => 'TW',
3003
            'TZA' => 'TZ',
3004
            'UGA' => 'UG',
3005
            'UKR' => 'UA',
3006
            'UMI' => 'UM',
3007
            'URY' => 'UY',
3008
            'USA' => 'US',
3009
            'UZB' => 'UZ',
3010
            'VAT' => 'VA',
3011
            'VCT' => 'VC',
3012
            'VEN' => 'VE',
3013
            'VGB' => 'VG',
3014
            'VIR' => 'VI',
3015
            'VNM' => 'VN',
3016
            'VUT' => 'VU',
3017
            'WLF' => 'WF',
3018
            'WLS' => 'GB',
3019
            'WSM' => 'WS',
3020
            'YEM' => 'YE',
3021
            'ZAF' => 'ZA',
3022
            'ZMB' => 'ZM',
3023
            'ZWE' => 'ZW',
3024
        ];
3025
    }
3026
3027
    /**
3028
     * Returns the translated country name based on the given two letter country code.
3029
     */
3030
    private function mapTwoLetterToName(string $twoLetterCode): string
3031
    {
3032
        $threeLetterCode = array_search($twoLetterCode, $this->iso3166(), true);
3033
        $threeLetterCode = $threeLetterCode ?: '???';
3034
3035
        return $this->getAllCountries()[$threeLetterCode];
3036
    }
3037
}
3038