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