xref: /webtrees/app/Statistics/Repository/IndividualRepository.php (revision dec352c1d7404cdd35c9b1a1b5d97f29e7c4ebb5)
1<?php
2
3/**
4 * webtrees: online genealogy
5 * Copyright (C) 2020 webtrees development team
6 * This program is free software: you can redistribute it and/or modify
7 * it under the terms of the GNU General Public License as published by
8 * the Free Software Foundation, either version 3 of the License, or
9 * (at your option) any later version.
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
14 * You should have received a copy of the GNU General Public License
15 * along with this program. If not, see <http://www.gnu.org/licenses/>.
16 */
17
18declare(strict_types=1);
19
20namespace Fisharebest\Webtrees\Statistics\Repository;
21
22use Fisharebest\Webtrees\Auth;
23use Fisharebest\Webtrees\Carbon;
24use Fisharebest\Webtrees\Factory;
25use Fisharebest\Webtrees\Functions\FunctionsPrintLists;
26use Fisharebest\Webtrees\Gedcom;
27use Fisharebest\Webtrees\GedcomRecord;
28use Fisharebest\Webtrees\I18N;
29use Fisharebest\Webtrees\Individual;
30use Fisharebest\Webtrees\Module\IndividualListModule;
31use Fisharebest\Webtrees\Module\ModuleInterface;
32use Fisharebest\Webtrees\Module\ModuleListInterface;
33use Fisharebest\Webtrees\Services\ModuleService;
34use Fisharebest\Webtrees\Statistics\Google\ChartAge;
35use Fisharebest\Webtrees\Statistics\Google\ChartBirth;
36use Fisharebest\Webtrees\Statistics\Google\ChartCommonGiven;
37use Fisharebest\Webtrees\Statistics\Google\ChartCommonSurname;
38use Fisharebest\Webtrees\Statistics\Google\ChartDeath;
39use Fisharebest\Webtrees\Statistics\Google\ChartFamilyWithSources;
40use Fisharebest\Webtrees\Statistics\Google\ChartIndividualWithSources;
41use Fisharebest\Webtrees\Statistics\Google\ChartMortality;
42use Fisharebest\Webtrees\Statistics\Google\ChartSex;
43use Fisharebest\Webtrees\Statistics\Repository\Interfaces\IndividualRepositoryInterface;
44use Fisharebest\Webtrees\Tree;
45use Illuminate\Database\Capsule\Manager as DB;
46use Illuminate\Database\Query\Builder;
47use Illuminate\Database\Query\Expression;
48use Illuminate\Database\Query\JoinClause;
49use stdClass;
50
51use function array_key_exists;
52use function array_slice;
53
54/**
55 *
56 */
57class IndividualRepository implements IndividualRepositoryInterface
58{
59    /**
60     * @var Tree
61     */
62    private $tree;
63
64    /**
65     * Constructor.
66     *
67     * @param Tree $tree
68     */
69    public function __construct(Tree $tree)
70    {
71        $this->tree = $tree;
72    }
73
74    /**
75     * Find common given names.
76     *
77     * @param string $sex
78     * @param string $type
79     * @param bool   $show_tot
80     * @param int    $threshold
81     * @param int    $maxtoshow
82     *
83     * @return string|int[]
84     */
85    private function commonGivenQuery(string $sex, string $type, bool $show_tot, int $threshold, int $maxtoshow)
86    {
87        $query = DB::table('name')
88            ->join('individuals', static function (JoinClause $join): void {
89                $join
90                    ->on('i_file', '=', 'n_file')
91                    ->on('i_id', '=', 'n_id');
92            })
93            ->where('n_file', '=', $this->tree->id())
94            ->where('n_type', '<>', '_MARNM')
95            ->where('n_givn', '<>', '@P.N.')
96            ->where(new Expression('LENGTH(n_givn)'), '>', 1);
97
98        switch ($sex) {
99            case 'M':
100            case 'F':
101            case 'U':
102                $query->where('i_sex', '=', $sex);
103                break;
104
105            case 'B':
106            default:
107                $query->where('i_sex', '<>', 'U');
108                break;
109        }
110
111        $rows = $query
112            ->groupBy(['n_givn'])
113            ->select(['n_givn', new Expression('COUNT(distinct n_id) AS count')])
114            ->pluck('count', 'n_givn');
115
116        $nameList = [];
117
118        foreach ($rows as $n_givn => $count) {
119            // Split “John Thomas” into “John” and “Thomas” and count against both totals
120            foreach (explode(' ', (string) $n_givn) as $given) {
121                // Exclude initials and particles.
122                if (!preg_match('/^([A-Z]|[a-z]{1,3})$/', $given)) {
123                    if (array_key_exists($given, $nameList)) {
124                        $nameList[$given] += (int) $count;
125                    } else {
126                        $nameList[$given] = (int) $count;
127                    }
128                }
129            }
130        }
131        arsort($nameList);
132        $nameList = array_slice($nameList, 0, $maxtoshow);
133
134        foreach ($nameList as $given => $total) {
135            if ($total < $threshold) {
136                unset($nameList[$given]);
137            }
138        }
139
140        switch ($type) {
141            case 'chart':
142                return $nameList;
143
144            case 'table':
145                return view('lists/given-names-table', [
146                    'given_names' => $nameList,
147                ]);
148
149            case 'list':
150                return view('lists/given-names-list', [
151                    'given_names' => $nameList,
152                    'show_totals' => $show_tot,
153                ]);
154
155            case 'nolist':
156            default:
157                array_walk($nameList, static function (string &$value, string $key) use ($show_tot): void {
158                    if ($show_tot) {
159                        $value = '<span dir="auto">' . e($key) . '</span>';
160                    } else {
161                        $value = '<span dir="auto">' . e($key) . '</span> (' . I18N::number((int) $value) . ')';
162                    }
163                });
164
165                return implode(I18N::$list_separator, $nameList);
166        }
167    }
168
169    /**
170     * Find common give names.
171     *
172     * @param int $threshold
173     * @param int $maxtoshow
174     *
175     * @return string
176     */
177    public function commonGiven(int $threshold = 1, int $maxtoshow = 10): string
178    {
179        return $this->commonGivenQuery('B', 'nolist', false, $threshold, $maxtoshow);
180    }
181
182    /**
183     * Find common give names.
184     *
185     * @param int $threshold
186     * @param int $maxtoshow
187     *
188     * @return string
189     */
190    public function commonGivenTotals(int $threshold = 1, int $maxtoshow = 10): string
191    {
192        return $this->commonGivenQuery('B', 'nolist', true, $threshold, $maxtoshow);
193    }
194
195    /**
196     * Find common give names.
197     *
198     * @param int $threshold
199     * @param int $maxtoshow
200     *
201     * @return string
202     */
203    public function commonGivenList(int $threshold = 1, int $maxtoshow = 10): string
204    {
205        return $this->commonGivenQuery('B', 'list', false, $threshold, $maxtoshow);
206    }
207
208    /**
209     * Find common give names.
210     *
211     * @param int $threshold
212     * @param int $maxtoshow
213     *
214     * @return string
215     */
216    public function commonGivenListTotals(int $threshold = 1, int $maxtoshow = 10): string
217    {
218        return $this->commonGivenQuery('B', 'list', true, $threshold, $maxtoshow);
219    }
220
221    /**
222     * Find common give names.
223     *
224     * @param int $threshold
225     * @param int $maxtoshow
226     *
227     * @return string
228     */
229    public function commonGivenTable(int $threshold = 1, int $maxtoshow = 10): string
230    {
231        return $this->commonGivenQuery('B', 'table', false, $threshold, $maxtoshow);
232    }
233
234    /**
235     * Find common give names of females.
236     *
237     * @param int $threshold
238     * @param int $maxtoshow
239     *
240     * @return string
241     */
242    public function commonGivenFemale(int $threshold = 1, int $maxtoshow = 10): string
243    {
244        return $this->commonGivenQuery('F', 'nolist', false, $threshold, $maxtoshow);
245    }
246
247    /**
248     * Find common give names of females.
249     *
250     * @param int $threshold
251     * @param int $maxtoshow
252     *
253     * @return string
254     */
255    public function commonGivenFemaleTotals(int $threshold = 1, int $maxtoshow = 10): string
256    {
257        return $this->commonGivenQuery('F', 'nolist', true, $threshold, $maxtoshow);
258    }
259
260    /**
261     * Find common give names of females.
262     *
263     * @param int $threshold
264     * @param int $maxtoshow
265     *
266     * @return string
267     */
268    public function commonGivenFemaleList(int $threshold = 1, int $maxtoshow = 10): string
269    {
270        return $this->commonGivenQuery('F', 'list', false, $threshold, $maxtoshow);
271    }
272
273    /**
274     * Find common give names of females.
275     *
276     * @param int $threshold
277     * @param int $maxtoshow
278     *
279     * @return string
280     */
281    public function commonGivenFemaleListTotals(int $threshold = 1, int $maxtoshow = 10): string
282    {
283        return $this->commonGivenQuery('F', 'list', true, $threshold, $maxtoshow);
284    }
285
286    /**
287     * Find common give names of females.
288     *
289     * @param int $threshold
290     * @param int $maxtoshow
291     *
292     * @return string
293     */
294    public function commonGivenFemaleTable(int $threshold = 1, int $maxtoshow = 10): string
295    {
296        return $this->commonGivenQuery('F', 'table', false, $threshold, $maxtoshow);
297    }
298
299    /**
300     * Find common give names of males.
301     *
302     * @param int $threshold
303     * @param int $maxtoshow
304     *
305     * @return string
306     */
307    public function commonGivenMale(int $threshold = 1, int $maxtoshow = 10): string
308    {
309        return $this->commonGivenQuery('M', 'nolist', false, $threshold, $maxtoshow);
310    }
311
312    /**
313     * Find common give names of males.
314     *
315     * @param int $threshold
316     * @param int $maxtoshow
317     *
318     * @return string
319     */
320    public function commonGivenMaleTotals(int $threshold = 1, int $maxtoshow = 10): string
321    {
322        return $this->commonGivenQuery('M', 'nolist', true, $threshold, $maxtoshow);
323    }
324
325    /**
326     * Find common give names of males.
327     *
328     * @param int $threshold
329     * @param int $maxtoshow
330     *
331     * @return string
332     */
333    public function commonGivenMaleList(int $threshold = 1, int $maxtoshow = 10): string
334    {
335        return $this->commonGivenQuery('M', 'list', false, $threshold, $maxtoshow);
336    }
337
338    /**
339     * Find common give names of males.
340     *
341     * @param int $threshold
342     * @param int $maxtoshow
343     *
344     * @return string
345     */
346    public function commonGivenMaleListTotals(int $threshold = 1, int $maxtoshow = 10): string
347    {
348        return $this->commonGivenQuery('M', 'list', true, $threshold, $maxtoshow);
349    }
350
351    /**
352     * Find common give names of males.
353     *
354     * @param int $threshold
355     * @param int $maxtoshow
356     *
357     * @return string
358     */
359    public function commonGivenMaleTable(int $threshold = 1, int $maxtoshow = 10): string
360    {
361        return $this->commonGivenQuery('M', 'table', false, $threshold, $maxtoshow);
362    }
363
364    /**
365     * Find common give names of unknown sexes.
366     *
367     * @param int $threshold
368     * @param int $maxtoshow
369     *
370     * @return string
371     */
372    public function commonGivenUnknown(int $threshold = 1, int $maxtoshow = 10): string
373    {
374        return $this->commonGivenQuery('U', 'nolist', false, $threshold, $maxtoshow);
375    }
376
377    /**
378     * Find common give names of unknown sexes.
379     *
380     * @param int $threshold
381     * @param int $maxtoshow
382     *
383     * @return string
384     */
385    public function commonGivenUnknownTotals(int $threshold = 1, int $maxtoshow = 10): string
386    {
387        return $this->commonGivenQuery('U', 'nolist', true, $threshold, $maxtoshow);
388    }
389
390    /**
391     * Find common give names of unknown sexes.
392     *
393     * @param int $threshold
394     * @param int $maxtoshow
395     *
396     * @return string
397     */
398    public function commonGivenUnknownList(int $threshold = 1, int $maxtoshow = 10): string
399    {
400        return $this->commonGivenQuery('U', 'list', false, $threshold, $maxtoshow);
401    }
402
403    /**
404     * Find common give names of unknown sexes.
405     *
406     * @param int $threshold
407     * @param int $maxtoshow
408     *
409     * @return string
410     */
411    public function commonGivenUnknownListTotals(int $threshold = 1, int $maxtoshow = 10): string
412    {
413        return $this->commonGivenQuery('U', 'list', true, $threshold, $maxtoshow);
414    }
415
416    /**
417     * Find common give names of unknown sexes.
418     *
419     * @param int $threshold
420     * @param int $maxtoshow
421     *
422     * @return string
423     */
424    public function commonGivenUnknownTable(int $threshold = 1, int $maxtoshow = 10): string
425    {
426        return $this->commonGivenQuery('U', 'table', false, $threshold, $maxtoshow);
427    }
428
429    /**
430     * Count the number of distinct given names (or the number of occurences of specific given names).
431     *
432     * @param string[] ...$params
433     *
434     * @return string
435     */
436    public function totalGivennames(...$params): string
437    {
438        $query = DB::table('name')
439            ->where('n_file', '=', $this->tree->id());
440
441        if ($params === []) {
442            // Count number of distinct given names.
443            $query
444                ->distinct()
445                ->where('n_givn', '<>', '@P.N.')
446                ->whereNotNull('n_givn');
447        } else {
448            // Count number of occurences of specific given names.
449            $query->whereIn('n_givn', $params);
450        }
451
452        $count = $query->count('n_givn');
453
454        return I18N::number($count);
455    }
456
457    /**
458     * Count the number of distinct surnames (or the number of occurrences of specific surnames).
459     *
460     * @param string[] ...$params
461     *
462     * @return string
463     */
464    public function totalSurnames(...$params): string
465    {
466        $query = DB::table('name')
467            ->where('n_file', '=', $this->tree->id());
468
469        if ($params === []) {
470            // Count number of distinct surnames
471            $query->distinct()
472                ->whereNotNull('n_surn');
473        } else {
474            // Count number of occurences of specific surnames.
475            $query->whereIn('n_surn', $params);
476        }
477
478        $count = $query->count('n_surn');
479
480        return I18N::number($count);
481    }
482
483    /**
484     * @param int $number_of_surnames
485     * @param int $threshold
486     *
487     * @return int[][]
488     */
489    private function topSurnames(int $number_of_surnames, int $threshold): array
490    {
491        // Use the count of base surnames.
492        $top_surnames = DB::table('name')
493            ->where('n_file', '=', $this->tree->id())
494            ->where('n_type', '<>', '_MARNM')
495            ->whereNotIn('n_surn', ['', '@N.N.'])
496            ->select(['n_surn'])
497            ->groupBy(['n_surn'])
498            ->orderByRaw('count(n_surn) desc')
499            ->take($number_of_surnames)
500            ->get()
501            ->pluck('n_surn')
502            ->all();
503
504        $surnames = [];
505        foreach ($top_surnames as $top_surname) {
506            $variants = DB::table('name')
507                ->where('n_file', '=', $this->tree->id())
508                ->where(new Expression('n_surn /* COLLATE ' . I18N::collation() . ' */'), '=', $top_surname)
509                ->select(['n_surn', new Expression('COUNT(*) AS count')])
510                ->groupBy(['n_surn'])
511                ->get()
512                ->pluck('count', 'n_surn')
513                ->all();
514
515            if (array_sum($variants) > $threshold) {
516                $surnames[$top_surname] = $variants;
517            }
518        }
519
520        return $surnames;
521    }
522
523    /**
524     * Find common surnames.
525     *
526     * @return string
527     */
528    public function getCommonSurname(): string
529    {
530        $top_surname = $this->topSurnames(1, 0);
531
532        return $top_surname
533            ? implode(', ', array_keys(array_shift($top_surname)) ?? [])
534            : '';
535    }
536
537    /**
538     * Find common surnames.
539     *
540     * @param string $type
541     * @param bool   $show_tot
542     * @param int    $threshold
543     * @param int    $number_of_surnames
544     * @param string $sorting
545     *
546     * @return string
547     */
548    private function commonSurnamesQuery(
549        string $type,
550        bool $show_tot,
551        int $threshold,
552        int $number_of_surnames,
553        string $sorting
554    ): string {
555        $surnames = $this->topSurnames($number_of_surnames, $threshold);
556
557        switch ($sorting) {
558            default:
559            case 'alpha':
560                uksort($surnames, [I18N::class, 'strcasecmp']);
561                break;
562            case 'count':
563                break;
564            case 'rcount':
565                $surnames = array_reverse($surnames, true);
566                break;
567        }
568
569        //find a module providing individual lists
570        $module = app(ModuleService::class)->findByComponent(ModuleListInterface::class, $this->tree, Auth::user())->first(static function (ModuleInterface $module): bool {
571            return $module instanceof IndividualListModule;
572        });
573
574        return FunctionsPrintLists::surnameList(
575            $surnames,
576            ($type === 'list' ? 1 : 2),
577            $show_tot,
578            $module,
579            $this->tree
580        );
581    }
582
583    /**
584     * Find common surnames.
585     *
586     * @param int    $threshold
587     * @param int    $number_of_surnames
588     * @param string $sorting
589     *
590     * @return string
591     */
592    public function commonSurnames(
593        int $threshold = 1,
594        int $number_of_surnames = 10,
595        string $sorting = 'alpha'
596    ): string {
597        return $this->commonSurnamesQuery('nolist', false, $threshold, $number_of_surnames, $sorting);
598    }
599
600    /**
601     * Find common surnames.
602     *
603     * @param int    $threshold
604     * @param int    $number_of_surnames
605     * @param string $sorting
606     *
607     * @return string
608     */
609    public function commonSurnamesTotals(
610        int $threshold = 1,
611        int $number_of_surnames = 10,
612        string $sorting = 'rcount'
613    ): string {
614        return $this->commonSurnamesQuery('nolist', true, $threshold, $number_of_surnames, $sorting);
615    }
616
617    /**
618     * Find common surnames.
619     *
620     * @param int    $threshold
621     * @param int    $number_of_surnames
622     * @param string $sorting
623     *
624     * @return string
625     */
626    public function commonSurnamesList(
627        int $threshold = 1,
628        int $number_of_surnames = 10,
629        string $sorting = 'alpha'
630    ): string {
631        return $this->commonSurnamesQuery('list', false, $threshold, $number_of_surnames, $sorting);
632    }
633
634    /**
635     * Find common surnames.
636     *
637     * @param int    $threshold
638     * @param int    $number_of_surnames
639     * @param string $sorting
640     *
641     * @return string
642     */
643    public function commonSurnamesListTotals(
644        int $threshold = 1,
645        int $number_of_surnames = 10,
646        string $sorting = 'rcount'
647    ): string {
648        return $this->commonSurnamesQuery('list', true, $threshold, $number_of_surnames, $sorting);
649    }
650
651    /**
652     * Get a count of births by month.
653     *
654     * @param int  $year1
655     * @param int  $year2
656     *
657     * @return Builder
658     */
659    public function statsBirthQuery(int $year1 = -1, int $year2 = -1): Builder
660    {
661        $query = DB::table('dates')
662            ->select(['d_month', new Expression('COUNT(*) AS total')])
663            ->where('d_file', '=', $this->tree->id())
664            ->where('d_fact', '=', 'BIRT')
665            ->whereIn('d_type', ['@#DGREGORIAN@', '@#DJULIAN@'])
666            ->groupBy(['d_month']);
667
668        if ($year1 >= 0 && $year2 >= 0) {
669            $query->whereBetween('d_year', [$year1, $year2]);
670        }
671
672        return $query;
673    }
674
675    /**
676     * Get a count of births by month.
677     *
678     * @param int  $year1
679     * @param int  $year2
680     *
681     * @return Builder
682     */
683    public function statsBirthBySexQuery(int $year1 = -1, int $year2 = -1): Builder
684    {
685        return $this->statsBirthQuery($year1, $year2)
686                ->select(['d_month', 'i_sex', new Expression('COUNT(*) AS total')])
687                ->join('individuals', static function (JoinClause $join): void {
688                    $join
689                        ->on('i_id', '=', 'd_gid')
690                        ->on('i_file', '=', 'd_file');
691                })
692                ->groupBy(['i_sex']);
693    }
694
695    /**
696     * General query on births.
697     *
698     * @param string|null $color_from
699     * @param string|null $color_to
700     *
701     * @return string
702     */
703    public function statsBirth(string $color_from = null, string $color_to = null): string
704    {
705        return (new ChartBirth($this->tree))
706            ->chartBirth($color_from, $color_to);
707    }
708
709    /**
710     * Get a list of death dates.
711     *
712     * @param int  $year1
713     * @param int  $year2
714     *
715     * @return Builder
716     */
717    public function statsDeathQuery(int $year1 = -1, int $year2 = -1): Builder
718    {
719        $query = DB::table('dates')
720            ->select(['d_month', new Expression('COUNT(*) AS total')])
721            ->where('d_file', '=', $this->tree->id())
722            ->where('d_fact', '=', 'DEAT')
723            ->whereIn('d_type', ['@#DGREGORIAN@', '@#DJULIAN@'])
724            ->groupBy(['d_month']);
725
726        if ($year1 >= 0 && $year2 >= 0) {
727            $query->whereBetween('d_year', [$year1, $year2]);
728        }
729
730        return $query;
731    }
732
733    /**
734     * Get a list of death dates.
735     *
736     * @param int  $year1
737     * @param int  $year2
738     *
739     * @return Builder
740     */
741    public function statsDeathBySexQuery(int $year1 = -1, int $year2 = -1): Builder
742    {
743        return $this->statsDeathQuery($year1, $year2)
744                ->select(['d_month', 'i_sex', new Expression('COUNT(*) AS total')])
745                ->join('individuals', static function (JoinClause $join): void {
746                    $join
747                        ->on('i_id', '=', 'd_gid')
748                        ->on('i_file', '=', 'd_file');
749                })
750                ->groupBy(['i_sex']);
751    }
752
753    /**
754     * General query on deaths.
755     *
756     * @param string|null $color_from
757     * @param string|null $color_to
758     *
759     * @return string
760     */
761    public function statsDeath(string $color_from = null, string $color_to = null): string
762    {
763        return (new ChartDeath($this->tree))
764            ->chartDeath($color_from, $color_to);
765    }
766
767    /**
768     * General query on ages.
769     *
770     * @param string $related
771     * @param string $sex
772     * @param int    $year1
773     * @param int    $year2
774     *
775     * @return array<stdClass>
776     */
777    public function statsAgeQuery(string $related = 'BIRT', string $sex = 'BOTH', int $year1 = -1, int $year2 = -1)
778    {
779        $prefix = DB::connection()->getTablePrefix();
780
781        $query = $this->birthAndDeathQuery($sex);
782
783        if ($year1 >= 0 && $year2 >= 0) {
784            $query
785                ->whereIn('birth.d_type', ['@#DGREGORIAN@', '@#DJULIAN@'])
786                ->whereIn('death.d_type', ['@#DGREGORIAN@', '@#DJULIAN@']);
787
788            if ($related === 'BIRT') {
789                $query->whereBetween('birth.d_year', [$year1, $year2]);
790            } elseif ($related === 'DEAT') {
791                $query->whereBetween('death.d_year', [$year1, $year2]);
792            }
793        }
794
795        return $query
796            ->select(new Expression($prefix . 'death.d_julianday2 - ' . $prefix . 'birth.d_julianday1 AS days'))
797            ->orderBy('days', 'desc')
798            ->get()
799            ->all();
800    }
801
802    /**
803     * General query on ages.
804     *
805     * @return string
806     */
807    public function statsAge(): string
808    {
809        return (new ChartAge($this->tree))->chartAge();
810    }
811
812    /**
813     * Lifespan
814     *
815     * @param string $type
816     * @param string $sex
817     *
818     * @return string
819     */
820    private function longlifeQuery(string $type, string $sex): string
821    {
822        $prefix = DB::connection()->getTablePrefix();
823
824        $row = $this->birthAndDeathQuery($sex)
825            ->orderBy('days', 'desc')
826            ->select(['individuals.*', new Expression($prefix . 'death.d_julianday2 - ' . $prefix . 'birth.d_julianday1 AS days')])
827            ->first();
828
829        if ($row === null) {
830            return '';
831        }
832
833        /** @var Individual $individual */
834        $individual = Factory::individual()->mapper($this->tree)($row);
835
836        if (!$individual->canShow()) {
837            return I18N::translate('This information is private and cannot be shown.');
838        }
839
840        switch ($type) {
841            default:
842            case 'full':
843                return $individual->formatList();
844
845            case 'age':
846                return I18N::number((int) ($row->days / 365.25));
847
848            case 'name':
849                return '<a href="' . e($individual->url()) . '">' . $individual->fullName() . '</a>';
850        }
851    }
852
853    /**
854     * Find the longest lived individual.
855     *
856     * @return string
857     */
858    public function longestLife(): string
859    {
860        return $this->longlifeQuery('full', 'BOTH');
861    }
862
863    /**
864     * Find the age of the longest lived individual.
865     *
866     * @return string
867     */
868    public function longestLifeAge(): string
869    {
870        return $this->longlifeQuery('age', 'BOTH');
871    }
872
873    /**
874     * Find the name of the longest lived individual.
875     *
876     * @return string
877     */
878    public function longestLifeName(): string
879    {
880        return $this->longlifeQuery('name', 'BOTH');
881    }
882
883    /**
884     * Find the longest lived female.
885     *
886     * @return string
887     */
888    public function longestLifeFemale(): string
889    {
890        return $this->longlifeQuery('full', 'F');
891    }
892
893    /**
894     * Find the age of the longest lived female.
895     *
896     * @return string
897     */
898    public function longestLifeFemaleAge(): string
899    {
900        return $this->longlifeQuery('age', 'F');
901    }
902
903    /**
904     * Find the name of the longest lived female.
905     *
906     * @return string
907     */
908    public function longestLifeFemaleName(): string
909    {
910        return $this->longlifeQuery('name', 'F');
911    }
912
913    /**
914     * Find the longest lived male.
915     *
916     * @return string
917     */
918    public function longestLifeMale(): string
919    {
920        return $this->longlifeQuery('full', 'M');
921    }
922
923    /**
924     * Find the age of the longest lived male.
925     *
926     * @return string
927     */
928    public function longestLifeMaleAge(): string
929    {
930        return $this->longlifeQuery('age', 'M');
931    }
932
933    /**
934     * Find the name of the longest lived male.
935     *
936     * @return string
937     */
938    public function longestLifeMaleName(): string
939    {
940        return $this->longlifeQuery('name', 'M');
941    }
942
943    /**
944     * Returns the calculated age the time of event.
945     *
946     * @param int $days The age from the database record
947     *
948     * @return string
949     */
950    private function calculateAge(int $days): string
951    {
952        if ($days < 31) {
953            return I18N::plural('%s day', '%s days', $days, I18N::number($days));
954        }
955
956        if ($days < 365) {
957            $months = (int) ($days / 30.5);
958            return I18N::plural('%s month', '%s months', $months, I18N::number($months));
959        }
960
961        $years = (int) ($days / 365.25);
962
963        return I18N::plural('%s year', '%s years', $years, I18N::number($years));
964    }
965
966    /**
967     * Find the oldest individuals.
968     *
969     * @param string $sex
970     * @param int    $total
971     *
972     * @return array<array<string,mixed>>
973     */
974    private function topTenOldestQuery(string $sex, int $total): array
975    {
976        $prefix = DB::connection()->getTablePrefix();
977
978        $rows = $this->birthAndDeathQuery($sex)
979            ->groupBy(['i_id', 'i_file'])
980            ->orderBy('days', 'desc')
981            ->select(['individuals.*', new Expression('MAX(' . $prefix . 'death.d_julianday2 - ' . $prefix . 'birth.d_julianday1) AS days')])
982            ->take($total)
983            ->get();
984
985        $top10 = [];
986        foreach ($rows as $row) {
987            /** @var Individual $individual */
988            $individual = Factory::individual()->mapper($this->tree)($row);
989
990            if ($individual->canShow()) {
991                $top10[] = [
992                    'person' => $individual,
993                    'age'    => $this->calculateAge((int) $row->days),
994                ];
995            }
996        }
997
998        return $top10;
999    }
1000
1001    /**
1002     * Find the oldest individuals.
1003     *
1004     * @param int $total
1005     *
1006     * @return string
1007     */
1008    public function topTenOldest(int $total = 10): string
1009    {
1010        $records = $this->topTenOldestQuery('BOTH', $total);
1011
1012        return view('statistics/individuals/top10-nolist', [
1013            'records' => $records,
1014        ]);
1015    }
1016
1017    /**
1018     * Find the oldest living individuals.
1019     *
1020     * @param int $total
1021     *
1022     * @return string
1023     */
1024    public function topTenOldestList(int $total = 10): string
1025    {
1026        $records = $this->topTenOldestQuery('BOTH', $total);
1027
1028        return view('statistics/individuals/top10-list', [
1029            'records' => $records,
1030        ]);
1031    }
1032
1033    /**
1034     * Find the oldest females.
1035     *
1036     * @param int $total
1037     *
1038     * @return string
1039     */
1040    public function topTenOldestFemale(int $total = 10): string
1041    {
1042        $records = $this->topTenOldestQuery('F', $total);
1043
1044        return view('statistics/individuals/top10-nolist', [
1045            'records' => $records,
1046        ]);
1047    }
1048
1049    /**
1050     * Find the oldest living females.
1051     *
1052     * @param int $total
1053     *
1054     * @return string
1055     */
1056    public function topTenOldestFemaleList(int $total = 10): string
1057    {
1058        $records = $this->topTenOldestQuery('F', $total);
1059
1060        return view('statistics/individuals/top10-list', [
1061            'records' => $records,
1062        ]);
1063    }
1064
1065    /**
1066     * Find the longest lived males.
1067     *
1068     * @param int $total
1069     *
1070     * @return string
1071     */
1072    public function topTenOldestMale(int $total = 10): string
1073    {
1074        $records = $this->topTenOldestQuery('M', $total);
1075
1076        return view('statistics/individuals/top10-nolist', [
1077            'records' => $records,
1078        ]);
1079    }
1080
1081    /**
1082     * Find the longest lived males.
1083     *
1084     * @param int $total
1085     *
1086     * @return string
1087     */
1088    public function topTenOldestMaleList(int $total = 10): string
1089    {
1090        $records = $this->topTenOldestQuery('M', $total);
1091
1092        return view('statistics/individuals/top10-list', [
1093            'records' => $records,
1094        ]);
1095    }
1096
1097    /**
1098     * Find the oldest living individuals.
1099     *
1100     * @param string $sex   "M", "F" or "BOTH"
1101     * @param int    $total
1102     *
1103     * @return array<array<string,mixed>>
1104     */
1105    private function topTenOldestAliveQuery(string $sex, int $total): array
1106    {
1107        $query = DB::table('dates')
1108            ->join('individuals', static function (JoinClause $join): void {
1109                $join
1110                    ->on('i_id', '=', 'd_gid')
1111                    ->on('i_file', '=', 'd_file');
1112            })
1113            ->where('d_file', '=', $this->tree->id())
1114            ->where('d_julianday1', '<>', 0)
1115            ->where('d_fact', '=', 'BIRT')
1116            ->where('i_gedcom', 'NOT LIKE', "%\n1 DEAT%")
1117            ->where('i_gedcom', 'NOT LIKE', "%\n1 BURI%")
1118            ->where('i_gedcom', 'NOT LIKE', "%\n1 CREM%");
1119
1120        if ($sex === 'F' || $sex === 'M') {
1121            $query->where('i_sex', '=', $sex);
1122        }
1123
1124        return $query
1125            ->groupBy(['i_id', 'i_file'])
1126            ->orderBy(new Expression('MIN(d_julianday1)'))
1127            ->select(['individuals.*'])
1128            ->take($total)
1129            ->get()
1130            ->map(Factory::individual()->mapper($this->tree))
1131            ->filter(GedcomRecord::accessFilter())
1132            ->map(function (Individual $individual): array {
1133                return [
1134                    'person' => $individual,
1135                    'age'    => $this->calculateAge(Carbon::now()->julianDay() - $individual->getBirthDate()->minimumJulianDay()),
1136                ];
1137            })
1138            ->all();
1139    }
1140
1141    /**
1142     * Find the oldest living individuals.
1143     *
1144     * @param int $total
1145     *
1146     * @return string
1147     */
1148    public function topTenOldestAlive(int $total = 10): string
1149    {
1150        if (!Auth::isMember($this->tree)) {
1151            return I18N::translate('This information is private and cannot be shown.');
1152        }
1153
1154        $records = $this->topTenOldestAliveQuery('BOTH', $total);
1155
1156        return view('statistics/individuals/top10-nolist', [
1157            'records' => $records,
1158        ]);
1159    }
1160
1161    /**
1162     * Find the oldest living individuals.
1163     *
1164     * @param int $total
1165     *
1166     * @return string
1167     */
1168    public function topTenOldestListAlive(int $total = 10): string
1169    {
1170        if (!Auth::isMember($this->tree)) {
1171            return I18N::translate('This information is private and cannot be shown.');
1172        }
1173
1174        $records = $this->topTenOldestAliveQuery('BOTH', $total);
1175
1176        return view('statistics/individuals/top10-list', [
1177            'records' => $records,
1178        ]);
1179    }
1180
1181    /**
1182     * Find the oldest living females.
1183     *
1184     * @param int $total
1185     *
1186     * @return string
1187     */
1188    public function topTenOldestFemaleAlive(int $total = 10): string
1189    {
1190        if (!Auth::isMember($this->tree)) {
1191            return I18N::translate('This information is private and cannot be shown.');
1192        }
1193
1194        $records = $this->topTenOldestAliveQuery('F', $total);
1195
1196        return view('statistics/individuals/top10-nolist', [
1197            'records' => $records,
1198        ]);
1199    }
1200
1201    /**
1202     * Find the oldest living females.
1203     *
1204     * @param int $total
1205     *
1206     * @return string
1207     */
1208    public function topTenOldestFemaleListAlive(int $total = 10): string
1209    {
1210        if (!Auth::isMember($this->tree)) {
1211            return I18N::translate('This information is private and cannot be shown.');
1212        }
1213
1214        $records = $this->topTenOldestAliveQuery('F', $total);
1215
1216        return view('statistics/individuals/top10-list', [
1217            'records' => $records,
1218        ]);
1219    }
1220
1221    /**
1222     * Find the longest lived living males.
1223     *
1224     * @param int $total
1225     *
1226     * @return string
1227     */
1228    public function topTenOldestMaleAlive(int $total = 10): string
1229    {
1230        if (!Auth::isMember($this->tree)) {
1231            return I18N::translate('This information is private and cannot be shown.');
1232        }
1233
1234        $records = $this->topTenOldestAliveQuery('M', $total);
1235
1236        return view('statistics/individuals/top10-nolist', [
1237            'records' => $records,
1238        ]);
1239    }
1240
1241    /**
1242     * Find the longest lived living males.
1243     *
1244     * @param int $total
1245     *
1246     * @return string
1247     */
1248    public function topTenOldestMaleListAlive(int $total = 10): string
1249    {
1250        if (!Auth::isMember($this->tree)) {
1251            return I18N::translate('This information is private and cannot be shown.');
1252        }
1253
1254        $records = $this->topTenOldestAliveQuery('M', $total);
1255
1256        return view('statistics/individuals/top10-list', [
1257            'records' => $records,
1258        ]);
1259    }
1260
1261    /**
1262     * Find the average lifespan.
1263     *
1264     * @param string $sex        "M", "F" or "BOTH"
1265     * @param bool   $show_years
1266     *
1267     * @return string
1268     */
1269    private function averageLifespanQuery(string $sex, bool $show_years): string
1270    {
1271        $prefix = DB::connection()->getTablePrefix();
1272
1273        $days = (int) $this->birthAndDeathQuery($sex)
1274            ->select(new Expression('AVG(' . $prefix . 'death.d_julianday2 - ' . $prefix . 'birth.d_julianday1) AS days'))
1275            ->value('days');
1276
1277        if ($show_years) {
1278            return $this->calculateAge($days);
1279        }
1280
1281        return I18N::number((int) ($days / 365.25));
1282    }
1283
1284    /**
1285     * Find the average lifespan.
1286     *
1287     * @param bool $show_years
1288     *
1289     * @return string
1290     */
1291    public function averageLifespan($show_years = false): string
1292    {
1293        return $this->averageLifespanQuery('BOTH', $show_years);
1294    }
1295
1296    /**
1297     * Find the average lifespan of females.
1298     *
1299     * @param bool $show_years
1300     *
1301     * @return string
1302     */
1303    public function averageLifespanFemale($show_years = false): string
1304    {
1305        return $this->averageLifespanQuery('F', $show_years);
1306    }
1307
1308    /**
1309     * Find the average male lifespan.
1310     *
1311     * @param bool $show_years
1312     *
1313     * @return string
1314     */
1315    public function averageLifespanMale($show_years = false): string
1316    {
1317        return $this->averageLifespanQuery('M', $show_years);
1318    }
1319
1320    /**
1321     * Convert totals into percentages.
1322     *
1323     * @param int $count
1324     * @param int $total
1325     *
1326     * @return string
1327     */
1328    private function getPercentage(int $count, int $total): string
1329    {
1330        return ($total !== 0) ? I18N::percentage($count / $total, 1) : '';
1331    }
1332
1333    /**
1334     * Returns how many individuals exist in the tree.
1335     *
1336     * @return int
1337     */
1338    private function totalIndividualsQuery(): int
1339    {
1340        return DB::table('individuals')
1341            ->where('i_file', '=', $this->tree->id())
1342            ->count();
1343    }
1344
1345    /**
1346     * Count the number of living individuals.
1347     *
1348     * The totalLiving/totalDeceased queries assume that every dead person will
1349     * have a DEAT record. It will not include individuals who were born more
1350     * than MAX_ALIVE_AGE years ago, and who have no DEAT record.
1351     * A good reason to run the “Add missing DEAT records” batch-update!
1352     *
1353     * @return int
1354     */
1355    private function totalLivingQuery(): int
1356    {
1357        $query = DB::table('individuals')
1358            ->where('i_file', '=', $this->tree->id());
1359
1360        foreach (Gedcom::DEATH_EVENTS as $death_event) {
1361            $query->where('i_gedcom', 'NOT LIKE', "%\n1 " . $death_event . '%');
1362        }
1363
1364        return $query->count();
1365    }
1366
1367    /**
1368     * Count the number of dead individuals.
1369     *
1370     * @return int
1371     */
1372    private function totalDeceasedQuery(): int
1373    {
1374        return DB::table('individuals')
1375            ->where('i_file', '=', $this->tree->id())
1376            ->where(static function (Builder $query): void {
1377                foreach (Gedcom::DEATH_EVENTS as $death_event) {
1378                    $query->orWhere('i_gedcom', 'LIKE', "%\n1 " . $death_event . '%');
1379                }
1380            })
1381            ->count();
1382    }
1383
1384    /**
1385     * Returns the total count of a specific sex.
1386     *
1387     * @param string $sex The sex to query
1388     *
1389     * @return int
1390     */
1391    private function getTotalSexQuery(string $sex): int
1392    {
1393        return DB::table('individuals')
1394            ->where('i_file', '=', $this->tree->id())
1395            ->where('i_sex', '=', $sex)
1396            ->count();
1397    }
1398
1399    /**
1400     * Returns the total number of males.
1401     *
1402     * @return int
1403     */
1404    private function totalSexMalesQuery(): int
1405    {
1406        return $this->getTotalSexQuery('M');
1407    }
1408
1409    /**
1410     * Returns the total number of females.
1411     *
1412     * @return int
1413     */
1414    private function totalSexFemalesQuery(): int
1415    {
1416        return $this->getTotalSexQuery('F');
1417    }
1418
1419    /**
1420     * Returns the total number of individuals with unknown sex.
1421     *
1422     * @return int
1423     */
1424    private function totalSexUnknownQuery(): int
1425    {
1426        return $this->getTotalSexQuery('U');
1427    }
1428
1429    /**
1430     * Count the total families.
1431     *
1432     * @return int
1433     */
1434    private function totalFamiliesQuery(): int
1435    {
1436        return DB::table('families')
1437            ->where('f_file', '=', $this->tree->id())
1438            ->count();
1439    }
1440
1441    /**
1442     * How many individuals have one or more sources.
1443     *
1444     * @return int
1445     */
1446    private function totalIndisWithSourcesQuery(): int
1447    {
1448        return DB::table('individuals')
1449            ->select(['i_id'])
1450            ->distinct()
1451            ->join('link', static function (JoinClause $join): void {
1452                $join->on('i_id', '=', 'l_from')
1453                    ->on('i_file', '=', 'l_file');
1454            })
1455            ->where('l_file', '=', $this->tree->id())
1456            ->where('l_type', '=', 'SOUR')
1457            ->count('i_id');
1458    }
1459
1460    /**
1461     * Count the families with source records.
1462     *
1463     * @return int
1464     */
1465    private function totalFamsWithSourcesQuery(): int
1466    {
1467        return DB::table('families')
1468            ->select(['f_id'])
1469            ->distinct()
1470            ->join('link', static function (JoinClause $join): void {
1471                $join->on('f_id', '=', 'l_from')
1472                    ->on('f_file', '=', 'l_file');
1473            })
1474            ->where('l_file', '=', $this->tree->id())
1475            ->where('l_type', '=', 'SOUR')
1476            ->count('f_id');
1477    }
1478
1479    /**
1480     * Count the number of repositories.
1481     *
1482     * @return int
1483     */
1484    private function totalRepositoriesQuery(): int
1485    {
1486        return DB::table('other')
1487            ->where('o_file', '=', $this->tree->id())
1488            ->where('o_type', '=', 'REPO')
1489            ->count();
1490    }
1491
1492    /**
1493     * Count the total number of sources.
1494     *
1495     * @return int
1496     */
1497    private function totalSourcesQuery(): int
1498    {
1499        return DB::table('sources')
1500            ->where('s_file', '=', $this->tree->id())
1501            ->count();
1502    }
1503
1504    /**
1505     * Count the number of notes.
1506     *
1507     * @return int
1508     */
1509    private function totalNotesQuery(): int
1510    {
1511        return DB::table('other')
1512            ->where('o_file', '=', $this->tree->id())
1513            ->where('o_type', '=', 'NOTE')
1514            ->count();
1515    }
1516
1517    /**
1518     * Returns the total number of records.
1519     *
1520     * @return int
1521     */
1522    private function totalRecordsQuery(): int
1523    {
1524        return $this->totalIndividualsQuery()
1525            + $this->totalFamiliesQuery()
1526            + $this->totalNotesQuery()
1527            + $this->totalRepositoriesQuery()
1528            + $this->totalSourcesQuery();
1529    }
1530
1531    /**
1532     * @return string
1533     */
1534    public function totalRecords(): string
1535    {
1536        return I18N::number($this->totalRecordsQuery());
1537    }
1538
1539    /**
1540     * @return string
1541     */
1542    public function totalIndividuals(): string
1543    {
1544        return I18N::number($this->totalIndividualsQuery());
1545    }
1546
1547    /**
1548     * Count the number of living individuals.
1549     *
1550     * @return string
1551     */
1552    public function totalLiving(): string
1553    {
1554        return I18N::number($this->totalLivingQuery());
1555    }
1556
1557    /**
1558     * Count the number of dead individuals.
1559     *
1560     * @return string
1561     */
1562    public function totalDeceased(): string
1563    {
1564        return I18N::number($this->totalDeceasedQuery());
1565    }
1566
1567    /**
1568     * @return string
1569     */
1570    public function totalSexMales(): string
1571    {
1572        return I18N::number($this->totalSexMalesQuery());
1573    }
1574
1575    /**
1576     * @return string
1577     */
1578    public function totalSexFemales(): string
1579    {
1580        return I18N::number($this->totalSexFemalesQuery());
1581    }
1582
1583    /**
1584     * @return string
1585     */
1586    public function totalSexUnknown(): string
1587    {
1588        return I18N::number($this->totalSexUnknownQuery());
1589    }
1590
1591    /**
1592     * @return string
1593     */
1594    public function totalFamilies(): string
1595    {
1596        return I18N::number($this->totalFamiliesQuery());
1597    }
1598
1599    /**
1600     * How many individuals have one or more sources.
1601     *
1602     * @return string
1603     */
1604    public function totalIndisWithSources(): string
1605    {
1606        return I18N::number($this->totalIndisWithSourcesQuery());
1607    }
1608
1609    /**
1610     * Count the families with with source records.
1611     *
1612     * @return string
1613     */
1614    public function totalFamsWithSources(): string
1615    {
1616        return I18N::number($this->totalFamsWithSourcesQuery());
1617    }
1618
1619    /**
1620     * @return string
1621     */
1622    public function totalRepositories(): string
1623    {
1624        return I18N::number($this->totalRepositoriesQuery());
1625    }
1626
1627    /**
1628     * @return string
1629     */
1630    public function totalSources(): string
1631    {
1632        return I18N::number($this->totalSourcesQuery());
1633    }
1634
1635    /**
1636     * @return string
1637     */
1638    public function totalNotes(): string
1639    {
1640        return I18N::number($this->totalNotesQuery());
1641    }
1642
1643    /**
1644     * @return string
1645     */
1646    public function totalIndividualsPercentage(): string
1647    {
1648        return $this->getPercentage(
1649            $this->totalIndividualsQuery(),
1650            $this->totalRecordsQuery()
1651        );
1652    }
1653
1654    /**
1655     * @return string
1656     */
1657    public function totalFamiliesPercentage(): string
1658    {
1659        return $this->getPercentage(
1660            $this->totalFamiliesQuery(),
1661            $this->totalRecordsQuery()
1662        );
1663    }
1664
1665    /**
1666     * @return string
1667     */
1668    public function totalRepositoriesPercentage(): string
1669    {
1670        return $this->getPercentage(
1671            $this->totalRepositoriesQuery(),
1672            $this->totalRecordsQuery()
1673        );
1674    }
1675
1676    /**
1677     * @return string
1678     */
1679    public function totalSourcesPercentage(): string
1680    {
1681        return $this->getPercentage(
1682            $this->totalSourcesQuery(),
1683            $this->totalRecordsQuery()
1684        );
1685    }
1686
1687    /**
1688     * @return string
1689     */
1690    public function totalNotesPercentage(): string
1691    {
1692        return $this->getPercentage(
1693            $this->totalNotesQuery(),
1694            $this->totalRecordsQuery()
1695        );
1696    }
1697
1698    /**
1699     * @return string
1700     */
1701    public function totalLivingPercentage(): string
1702    {
1703        return $this->getPercentage(
1704            $this->totalLivingQuery(),
1705            $this->totalIndividualsQuery()
1706        );
1707    }
1708
1709    /**
1710     * @return string
1711     */
1712    public function totalDeceasedPercentage(): string
1713    {
1714        return $this->getPercentage(
1715            $this->totalDeceasedQuery(),
1716            $this->totalIndividualsQuery()
1717        );
1718    }
1719
1720    /**
1721     * @return string
1722     */
1723    public function totalSexMalesPercentage(): string
1724    {
1725        return $this->getPercentage(
1726            $this->totalSexMalesQuery(),
1727            $this->totalIndividualsQuery()
1728        );
1729    }
1730
1731    /**
1732     * @return string
1733     */
1734    public function totalSexFemalesPercentage(): string
1735    {
1736        return $this->getPercentage(
1737            $this->totalSexFemalesQuery(),
1738            $this->totalIndividualsQuery()
1739        );
1740    }
1741
1742    /**
1743     * @return string
1744     */
1745    public function totalSexUnknownPercentage(): string
1746    {
1747        return $this->getPercentage(
1748            $this->totalSexUnknownQuery(),
1749            $this->totalIndividualsQuery()
1750        );
1751    }
1752
1753    /**
1754     * Create a chart of common given names.
1755     *
1756     * @param string|null $color_from
1757     * @param string|null $color_to
1758     * @param int         $maxtoshow
1759     *
1760     * @return string
1761     */
1762    public function chartCommonGiven(
1763        string $color_from = null,
1764        string $color_to = null,
1765        int $maxtoshow = 7
1766    ): string {
1767        $tot_indi = $this->totalIndividualsQuery();
1768        $given    = $this->commonGivenQuery('B', 'chart', false, 1, $maxtoshow);
1769
1770        if ($given === []) {
1771            return I18N::translate('This information is not available.');
1772        }
1773
1774        return (new ChartCommonGiven())
1775            ->chartCommonGiven($tot_indi, $given, $color_from, $color_to);
1776    }
1777
1778    /**
1779     * Create a chart of common surnames.
1780     *
1781     * @param string|null $color_from
1782     * @param string|null $color_to
1783     * @param int         $number_of_surnames
1784     *
1785     * @return string
1786     */
1787    public function chartCommonSurnames(
1788        string $color_from = null,
1789        string $color_to = null,
1790        int $number_of_surnames = 10
1791    ): string {
1792        $tot_indi     = $this->totalIndividualsQuery();
1793        $all_surnames = $this->topSurnames($number_of_surnames, 0);
1794
1795        if ($all_surnames === []) {
1796            return I18N::translate('This information is not available.');
1797        }
1798
1799        return (new ChartCommonSurname($this->tree))
1800            ->chartCommonSurnames($tot_indi, $all_surnames, $color_from, $color_to);
1801    }
1802
1803    /**
1804     * Create a chart showing mortality.
1805     *
1806     * @param string|null $color_living
1807     * @param string|null $color_dead
1808     *
1809     * @return string
1810     */
1811    public function chartMortality(string $color_living = null, string $color_dead = null): string
1812    {
1813        $tot_l = $this->totalLivingQuery();
1814        $tot_d = $this->totalDeceasedQuery();
1815
1816        return (new ChartMortality())
1817            ->chartMortality($tot_l, $tot_d, $color_living, $color_dead);
1818    }
1819
1820    /**
1821     * Create a chart showing individuals with/without sources.
1822     *
1823     * @param string|null $color_from
1824     * @param string|null $color_to
1825     *
1826     * @return string
1827     */
1828    public function chartIndisWithSources(
1829        string $color_from = null,
1830        string $color_to = null
1831    ): string {
1832        $tot_indi        = $this->totalIndividualsQuery();
1833        $tot_indi_source = $this->totalIndisWithSourcesQuery();
1834
1835        return (new ChartIndividualWithSources())
1836            ->chartIndisWithSources($tot_indi, $tot_indi_source, $color_from, $color_to);
1837    }
1838
1839    /**
1840     * Create a chart of individuals with/without sources.
1841     *
1842     * @param string|null $color_from
1843     * @param string|null $color_to
1844     *
1845     * @return string
1846     */
1847    public function chartFamsWithSources(
1848        string $color_from = null,
1849        string $color_to = null
1850    ): string {
1851        $tot_fam        = $this->totalFamiliesQuery();
1852        $tot_fam_source = $this->totalFamsWithSourcesQuery();
1853
1854        return (new ChartFamilyWithSources())
1855            ->chartFamsWithSources($tot_fam, $tot_fam_source, $color_from, $color_to);
1856    }
1857
1858    /**
1859     * @param string|null $color_female
1860     * @param string|null $color_male
1861     * @param string|null $color_unknown
1862     *
1863     * @return string
1864     */
1865    public function chartSex(
1866        string $color_female = null,
1867        string $color_male = null,
1868        string $color_unknown = null
1869    ): string {
1870        $tot_m = $this->totalSexMalesQuery();
1871        $tot_f = $this->totalSexFemalesQuery();
1872        $tot_u = $this->totalSexUnknownQuery();
1873
1874        return (new ChartSex())
1875            ->chartSex($tot_m, $tot_f, $tot_u, $color_female, $color_male, $color_unknown);
1876    }
1877
1878    /**
1879     * Query individuals, with their births and deaths.
1880     *
1881     * @param string $sex
1882     *
1883     * @return Builder
1884     */
1885    private function birthAndDeathQuery(string $sex): Builder
1886    {
1887        $query = DB::table('individuals')
1888            ->where('i_file', '=', $this->tree->id())
1889            ->join('dates AS birth', static function (JoinClause $join): void {
1890                $join
1891                    ->on('birth.d_file', '=', 'i_file')
1892                    ->on('birth.d_gid', '=', 'i_id');
1893            })
1894            ->join('dates AS death', static function (JoinClause $join): void {
1895                $join
1896                    ->on('death.d_file', '=', 'i_file')
1897                    ->on('death.d_gid', '=', 'i_id');
1898            })
1899            ->where('birth.d_fact', '=', 'BIRT')
1900            ->where('death.d_fact', '=', 'DEAT')
1901            ->whereColumn('death.d_julianday1', '>=', 'birth.d_julianday2')
1902            ->where('birth.d_julianday2', '<>', 0);
1903
1904        if ($sex === 'M' || $sex === 'F') {
1905            $query->where('i_sex', '=', $sex);
1906        }
1907
1908        return $query;
1909    }
1910}
1911