StatisticsData::countPlaces()   A
last analyzed

Complexity

Conditions 4
Paths 6

Size

Total Lines 23
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Importance

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