xref: /webtrees/app/Statistics/Repository/IndividualRepository.php (revision e11ffd0c922a07c13f23d38c7d9c82edce5298f5)
1<?php
2/**
3 * webtrees: online genealogy
4 * Copyright (C) 2018 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\ChartFamily;
33use Fisharebest\Webtrees\Statistics\Google\ChartFamilyWithSources;
34use Fisharebest\Webtrees\Statistics\Google\ChartIndividual;
35use Fisharebest\Webtrees\Statistics\Google\ChartMortality;
36use Fisharebest\Webtrees\Statistics\Google\ChartSex;
37use Fisharebest\Webtrees\Statistics\Helper\Sql;
38use Fisharebest\Webtrees\Statistics\Repository\Interfaces\IndividualRepositoryInterface;
39use Fisharebest\Webtrees\Tree;
40use Illuminate\Database\Capsule\Manager as DB;
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 count the number of
425     * occurrences of a specific name or names.
426     *
427     * @param array ...$params
428     *
429     * @return string
430     */
431    public function totalGivennames(...$params): string
432    {
433        if ($params) {
434            $qs       = implode(',', array_fill(0, \count($params), '?'));
435            $params[] = $this->tree->id();
436            $total    = (int) Database::prepare(
437                "SELECT COUNT( n_givn) FROM `##name` WHERE n_givn IN ({$qs}) AND n_file=?"
438            )->execute(
439                $params
440            )->fetchOne();
441        } else {
442            $total = (int) Database::prepare(
443                "SELECT COUNT(DISTINCT n_givn) FROM `##name` WHERE n_givn IS NOT NULL AND n_file=?"
444            )->execute([
445                $this->tree->id(),
446            ])->fetchOne();
447        }
448
449        return I18N::number($total);
450    }
451
452    /**
453     * Count the surnames.
454     *
455     * @param array ...$params
456     *
457     * @return string
458     */
459    public function totalSurnames(...$params): string
460    {
461        if ($params) {
462            $opt      = 'IN (' . implode(',', array_fill(0, \count($params), '?')) . ')';
463            $distinct = '';
464        } else {
465            $opt      = "IS NOT NULL";
466            $distinct = 'DISTINCT';
467        }
468        $params[] = $this->tree->id();
469
470        $total = (int) Database::prepare(
471            "SELECT COUNT({$distinct} n_surn COLLATE '" . I18N::collation() . "')" .
472            " FROM `##name`" .
473            " WHERE n_surn COLLATE '" . I18N::collation() . "' {$opt} AND n_file=?"
474        )->execute(
475            $params
476        )->fetchOne();
477
478        return I18N::number($total);
479    }
480
481    /**
482     * @param int $number_of_surnames
483     * @param int $threshold
484     *
485     * @return \stdClass[]
486     */
487    private function topSurnames(int $number_of_surnames, int $threshold): array
488    {
489        // Use the count of base surnames.
490        $top_surnames = Database::prepare(
491            "SELECT n_surn FROM `##name`" .
492            " WHERE n_file = :tree_id AND n_type != '_MARNM' AND n_surn NOT IN ('@N.N.', '')" .
493            " GROUP BY n_surn" .
494            " ORDER BY COUNT(n_surn) DESC" .
495            " LIMIT :limit"
496        )->execute([
497            'tree_id' => $this->tree->id(),
498            'limit'   => $number_of_surnames,
499        ])->fetchOneColumn();
500
501        $surnames = [];
502        foreach ($top_surnames as $top_surname) {
503            $variants = Database::prepare(
504                "SELECT n_surname COLLATE utf8_bin, COUNT(*) FROM `##name` WHERE n_file = :tree_id AND n_surn COLLATE :collate = :surname GROUP BY 1"
505            )->execute([
506                'collate' => I18N::collation(),
507                'surname' => $top_surname,
508                'tree_id' => $this->tree->id(),
509            ])->fetchAssoc();
510
511            if (array_sum($variants) > $threshold) {
512                $surnames[$top_surname] = $variants;
513            }
514        }
515
516        return $surnames;
517    }
518
519    /**
520     * Find common surnames.
521     *
522     * @return string
523     */
524    public function getCommonSurname(): string
525    {
526        $top_surname = $this->topSurnames(1, 0);
527        return implode(', ', array_keys(array_shift($top_surname)) ?? []);
528    }
529
530    /**
531     * Find common surnames.
532     *
533     * @param string $type
534     * @param bool   $show_tot
535     * @param int    $threshold
536     * @param int    $number_of_surnames
537     * @param string $sorting
538     *
539     * @return string
540     */
541    private function commonSurnamesQuery(
542        string $type,
543        bool $show_tot,
544        int $threshold,
545        int $number_of_surnames,
546        string $sorting
547    ): string {
548        $surnames = $this->topSurnames($number_of_surnames, $threshold);
549
550        switch ($sorting) {
551            default:
552            case 'alpha':
553                uksort($surnames, [I18N::class, 'strcasecmp']);
554                break;
555            case 'count':
556                break;
557            case 'rcount':
558                $surnames = array_reverse($surnames, true);
559                break;
560        }
561
562        return FunctionsPrintLists::surnameList(
563            $surnames,
564            ($type === 'list' ? 1 : 2),
565            $show_tot,
566            'individual-list',
567            $this->tree
568        );
569    }
570
571    /**
572     * Find common surnames.
573     *
574     * @param int    $threshold
575     * @param int    $number_of_surnames
576     * @param string $sorting
577     *
578     * @return string
579     */
580    public function commonSurnames(
581        int $threshold = 1,
582        int $number_of_surnames = 10,
583        string $sorting = 'alpha'
584    ): string {
585        return $this->commonSurnamesQuery('nolist', false, $threshold, $number_of_surnames, $sorting);
586    }
587
588    /**
589     * Find common surnames.
590     *
591     * @param int    $threshold
592     * @param int    $number_of_surnames
593     * @param string $sorting
594     *
595     * @return string
596     */
597    public function commonSurnamesTotals(
598        int $threshold = 1,
599        int $number_of_surnames = 10,
600        string $sorting = 'rcount'
601    ): string {
602        return $this->commonSurnamesQuery('nolist', true, $threshold, $number_of_surnames, $sorting);
603    }
604
605    /**
606     * Find common surnames.
607     *
608     * @param int    $threshold
609     * @param int    $number_of_surnames
610     * @param string $sorting
611     *
612     * @return string
613     */
614    public function commonSurnamesList(
615        int $threshold = 1,
616        int $number_of_surnames = 10,
617        string $sorting = 'alpha'
618    ): string {
619        return $this->commonSurnamesQuery('list', false, $threshold, $number_of_surnames, $sorting);
620    }
621
622    /**
623     * Find common surnames.
624     *
625     * @param int    $threshold
626     * @param int    $number_of_surnames
627     * @param string $sorting
628     *
629     * @return string
630     */
631    public function commonSurnamesListTotals(
632        int $threshold = 1,
633        int $number_of_surnames = 10,
634        string $sorting = 'rcount'
635    ): string {
636        return $this->commonSurnamesQuery('list', true, $threshold, $number_of_surnames, $sorting);
637    }
638
639    /**
640     * Get a list of birth dates.
641     *
642     * @param bool $sex
643     * @param int  $year1
644     * @param int  $year2
645     *
646     * @return array
647     */
648    public function statsBirthQuery(bool $sex = false, int $year1 = -1, int $year2 = -1): array
649    {
650        if ($sex) {
651            $sql =
652                "SELECT d_month, i_sex, COUNT(*) AS total FROM `##dates` " .
653                "JOIN `##individuals` ON d_file = i_file AND d_gid = i_id " .
654                "WHERE " .
655                "d_file={$this->tree->id()} AND " .
656                "d_fact='BIRT' AND " .
657                "d_type IN ('@#DGREGORIAN@', '@#DJULIAN@')";
658        } else {
659            $sql =
660                "SELECT d_month, COUNT(*) AS total FROM `##dates` " .
661                "WHERE " .
662                "d_file={$this->tree->id()} AND " .
663                "d_fact='BIRT' AND " .
664                "d_type IN ('@#DGREGORIAN@', '@#DJULIAN@')";
665        }
666
667        if ($year1 >= 0 && $year2 >= 0) {
668            $sql .= " AND d_year BETWEEN '{$year1}' AND '{$year2}'";
669        }
670
671        $sql .= " GROUP BY d_month";
672
673        if ($sex) {
674            $sql .= ", i_sex";
675        }
676
677        return $this->runSql($sql);
678    }
679
680    /**
681     * General query on births.
682     *
683     * @param string|null $size
684     * @param string|null $color_from
685     * @param string|null $color_to
686     *
687     * @return string
688     */
689    public function statsBirth(string $size = null, string $color_from = null, string $color_to = null): string
690    {
691        return (new ChartBirth($this->tree))
692            ->chartBirth($size, $color_from, $color_to);
693    }
694
695    /**
696     * Get a list of death dates.
697     *
698     * @param bool $sex
699     * @param int  $year1
700     * @param int  $year2
701     *
702     * @return array
703     */
704    public function statsDeathQuery(bool $sex = false, int $year1 = -1, int $year2 = -1): array
705    {
706        if ($sex) {
707            $sql =
708                "SELECT d_month, i_sex, COUNT(*) AS total FROM `##dates` " .
709                "JOIN `##individuals` ON d_file = i_file AND d_gid = i_id " .
710                "WHERE " .
711                "d_file={$this->tree->id()} AND " .
712                "d_fact='DEAT' AND " .
713                "d_type IN ('@#DGREGORIAN@', '@#DJULIAN@')";
714        } else {
715            $sql =
716                "SELECT d_month, COUNT(*) AS total FROM `##dates` " .
717                "WHERE " .
718                "d_file={$this->tree->id()} AND " .
719                "d_fact='DEAT' AND " .
720                "d_type IN ('@#DGREGORIAN@', '@#DJULIAN@')";
721        }
722
723        if ($year1 >= 0 && $year2 >= 0) {
724            $sql .= " AND d_year BETWEEN '{$year1}' AND '{$year2}'";
725        }
726
727        $sql .= " GROUP BY d_month";
728
729        if ($sex) {
730            $sql .= ", i_sex";
731        }
732
733        return $this->runSql($sql);
734    }
735
736    /**
737     * General query on deaths.
738     *
739     * @param string|null $size
740     * @param string|null $color_from
741     * @param string|null $color_to
742     *
743     * @return string
744     */
745    public function statsDeath(string $size = null, string $color_from = null, string $color_to = null): string
746    {
747        return (new ChartDeath($this->tree))
748            ->chartDeath($size, $color_from, $color_to);
749    }
750
751    /**
752     * General query on ages.
753     *
754     * @param string $related
755     * @param string $sex
756     * @param int    $year1
757     * @param int    $year2
758     *
759     * @return array|string
760     */
761    public function statsAgeQuery(string $related = 'BIRT', string $sex = 'BOTH', int $year1 = -1, int $year2 = -1)
762    {
763        $sex_search = '';
764        $years      = '';
765
766        if ($sex === 'F') {
767            $sex_search = " AND i_sex='F'";
768        } elseif ($sex === 'M') {
769            $sex_search = " AND i_sex='M'";
770        }
771
772        if ($year1 >= 0 && $year2 >= 0) {
773            if ($related === 'BIRT') {
774                $years = " AND birth.d_year BETWEEN '{$year1}' AND '{$year2}'";
775            } elseif ($related === 'DEAT') {
776                $years = " AND death.d_year BETWEEN '{$year1}' AND '{$year2}'";
777            }
778        }
779
780        $rows = $this->runSql(
781            "SELECT" .
782            " death.d_julianday2-birth.d_julianday1 AS age" .
783            " FROM" .
784            " `##dates` AS death," .
785            " `##dates` AS birth," .
786            " `##individuals` AS indi" .
787            " WHERE" .
788            " indi.i_id=birth.d_gid AND" .
789            " birth.d_gid=death.d_gid AND" .
790            " death.d_file={$this->tree->id()} AND" .
791            " birth.d_file=death.d_file AND" .
792            " birth.d_file=indi.i_file AND" .
793            " birth.d_fact='BIRT' AND" .
794            " death.d_fact='DEAT' AND" .
795            " birth.d_julianday1 <> 0 AND" .
796            " birth.d_type IN ('@#DGREGORIAN@', '@#DJULIAN@') AND" .
797            " death.d_type IN ('@#DGREGORIAN@', '@#DJULIAN@') AND" .
798            " death.d_julianday1>birth.d_julianday2" .
799            $years .
800            $sex_search .
801            " ORDER BY age DESC"
802        );
803
804        return $rows;
805    }
806
807    /**
808     * General query on ages.
809     *
810     * @param string $size
811     *
812     * @return string
813     */
814    public function statsAge(string $size = '230x250'): string
815    {
816        return (new ChartAge($this->tree))->chartAge($size);
817    }
818
819    /**
820     * Lifespan
821     *
822     * @param string $type
823     * @param string $sex
824     *
825     * @return string
826     */
827    private function longlifeQuery(string $type, string $sex): string
828    {
829        $sex_search = ' 1=1';
830        if ($sex === 'F') {
831            $sex_search = " i_sex='F'";
832        } elseif ($sex === 'M') {
833            $sex_search = " i_sex='M'";
834        }
835
836        $rows = $this->runSql(
837            " SELECT" .
838            " death.d_gid AS id," .
839            " death.d_julianday2-birth.d_julianday1 AS age" .
840            " FROM" .
841            " `##dates` AS death," .
842            " `##dates` AS birth," .
843            " `##individuals` AS indi" .
844            " WHERE" .
845            " indi.i_id=birth.d_gid AND" .
846            " birth.d_gid=death.d_gid AND" .
847            " death.d_file={$this->tree->id()} AND" .
848            " birth.d_file=death.d_file AND" .
849            " birth.d_file=indi.i_file AND" .
850            " birth.d_fact='BIRT' AND" .
851            " death.d_fact='DEAT' AND" .
852            " birth.d_julianday1<>0 AND" .
853            " death.d_julianday1>birth.d_julianday2 AND" .
854            $sex_search .
855            " ORDER BY" .
856            " age DESC LIMIT 1"
857        );
858        if (!isset($rows[0])) {
859            return '';
860        }
861        $row    = $rows[0];
862        $person = Individual::getInstance($row->id, $this->tree);
863        switch ($type) {
864            default:
865            case 'full':
866                if ($person->canShowName()) {
867                    $result = $person->formatList();
868                } else {
869                    $result = I18N::translate('This information is private and cannot be shown.');
870                }
871                break;
872            case 'age':
873                $result = I18N::number((int) ($row->age / 365.25));
874                break;
875            case 'name':
876                $result = '<a href="' . e($person->url()) . '">' . $person->getFullName() . '</a>';
877                break;
878        }
879
880        return $result;
881    }
882
883    /**
884     * Find the longest lived individual.
885     *
886     * @return string
887     */
888    public function longestLife(): string
889    {
890        return $this->longlifeQuery('full', 'BOTH');
891    }
892
893    /**
894     * Find the age of the longest lived individual.
895     *
896     * @return string
897     */
898    public function longestLifeAge(): string
899    {
900        return $this->longlifeQuery('age', 'BOTH');
901    }
902
903    /**
904     * Find the name of the longest lived individual.
905     *
906     * @return string
907     */
908    public function longestLifeName(): string
909    {
910        return $this->longlifeQuery('name', 'BOTH');
911    }
912
913    /**
914     * Find the longest lived female.
915     *
916     * @return string
917     */
918    public function longestLifeFemale(): string
919    {
920        return $this->longlifeQuery('full', 'F');
921    }
922
923    /**
924     * Find the age of the longest lived female.
925     *
926     * @return string
927     */
928    public function longestLifeFemaleAge(): string
929    {
930        return $this->longlifeQuery('age', 'F');
931    }
932
933    /**
934     * Find the name of the longest lived female.
935     *
936     * @return string
937     */
938    public function longestLifeFemaleName(): string
939    {
940        return $this->longlifeQuery('name', 'F');
941    }
942
943    /**
944     * Find the longest lived male.
945     *
946     * @return string
947     */
948    public function longestLifeMale(): string
949    {
950        return $this->longlifeQuery('full', 'M');
951    }
952
953    /**
954     * Find the age of the longest lived male.
955     *
956     * @return string
957     */
958    public function longestLifeMaleAge(): string
959    {
960        return $this->longlifeQuery('age', 'M');
961    }
962
963    /**
964     * Find the name of the longest lived male.
965     *
966     * @return string
967     */
968    public function longestLifeMaleName(): string
969    {
970        return $this->longlifeQuery('name', 'M');
971    }
972
973    /**
974     * Returns the calculated age the time of event.
975     *
976     * @param int $age The age from the database record
977     *
978     * @return string
979     */
980    private function calculateAge(int $age): string
981    {
982        if ((int) ($age / 365.25) > 0) {
983            $result = (int) ($age / 365.25) . 'y';
984        } elseif ((int) ($age / 30.4375) > 0) {
985            $result = (int) ($age / 30.4375) . 'm';
986        } else {
987            $result = $age . 'd';
988        }
989
990        return FunctionsDate::getAgeAtEvent($result);
991    }
992
993    /**
994     * Find the oldest individuals.
995     *
996     * @param string $sex
997     * @param int    $total
998     *
999     * @return array
1000     */
1001    private function topTenOldestQuery(string $sex, int $total): array
1002    {
1003        if ($sex === 'F') {
1004            $sex_search = " AND i_sex='F' ";
1005        } elseif ($sex === 'M') {
1006            $sex_search = " AND i_sex='M' ";
1007        } else {
1008            $sex_search = '';
1009        }
1010
1011        $rows = $this->runSql(
1012            "SELECT " .
1013            " MAX(death.d_julianday2-birth.d_julianday1) AS age, " .
1014            " death.d_gid AS deathdate " .
1015            "FROM " .
1016            " `##dates` AS death, " .
1017            " `##dates` AS birth, " .
1018            " `##individuals` AS indi " .
1019            "WHERE " .
1020            " indi.i_id=birth.d_gid AND " .
1021            " birth.d_gid=death.d_gid AND " .
1022            " death.d_file={$this->tree->id()} AND " .
1023            " birth.d_file=death.d_file AND " .
1024            " birth.d_file=indi.i_file AND " .
1025            " birth.d_fact='BIRT' AND " .
1026            " death.d_fact='DEAT' AND " .
1027            " birth.d_julianday1<>0 AND " .
1028            " death.d_julianday1>birth.d_julianday2 " .
1029            $sex_search .
1030            "GROUP BY deathdate " .
1031            "ORDER BY age DESC " .
1032            "LIMIT " . $total
1033        );
1034
1035        if (!isset($rows[0])) {
1036            return [];
1037        }
1038
1039        $top10 = [];
1040        foreach ($rows as $row) {
1041            $person = Individual::getInstance($row->deathdate, $this->tree);
1042
1043            if ($person->canShow()) {
1044                $top10[] = [
1045                    'person' => $person,
1046                    'age'    => $this->calculateAge((int) $row->age),
1047                ];
1048            }
1049        }
1050
1051        // TODO
1052//        if (I18N::direction() === 'rtl') {
1053//            $top10 = str_replace([
1054//                '[',
1055//                ']',
1056//                '(',
1057//                ')',
1058//                '+',
1059//            ], [
1060//                '&rlm;[',
1061//                '&rlm;]',
1062//                '&rlm;(',
1063//                '&rlm;)',
1064//                '&rlm;+',
1065//            ], $top10);
1066//        }
1067
1068        return $top10;
1069    }
1070
1071    /**
1072     * Find the oldest individuals.
1073     *
1074     * @param int $total
1075     *
1076     * @return string
1077     */
1078    public function topTenOldest(int $total = 10): string
1079    {
1080        $records = $this->topTenOldestQuery('BOTH', $total);
1081
1082        return view(
1083            'statistics/individuals/top10-nolist',
1084            [
1085                'records' => $records,
1086            ]
1087        );
1088    }
1089
1090    /**
1091     * Find the oldest living individuals.
1092     *
1093     * @param int $total
1094     *
1095     * @return string
1096     */
1097    public function topTenOldestList(int $total = 10): string
1098    {
1099        $records = $this->topTenOldestQuery('BOTH', $total);
1100
1101        return view(
1102            'statistics/individuals/top10-list',
1103            [
1104                'records' => $records,
1105            ]
1106        );
1107    }
1108
1109    /**
1110     * Find the oldest females.
1111     *
1112     * @param int $total
1113     *
1114     * @return string
1115     */
1116    public function topTenOldestFemale(int $total = 10): string
1117    {
1118        $records = $this->topTenOldestQuery('F', $total);
1119
1120        return view(
1121            'statistics/individuals/top10-nolist',
1122            [
1123                'records' => $records,
1124            ]
1125        );
1126    }
1127
1128    /**
1129     * Find the oldest living females.
1130     *
1131     * @param int $total
1132     *
1133     * @return string
1134     */
1135    public function topTenOldestFemaleList(int $total = 10): string
1136    {
1137        $records = $this->topTenOldestQuery('F', $total);
1138
1139        return view(
1140            'statistics/individuals/top10-list',
1141            [
1142                'records' => $records,
1143            ]
1144        );
1145    }
1146
1147    /**
1148     * Find the longest lived males.
1149     *
1150     * @param int $total
1151     *
1152     * @return string
1153     */
1154    public function topTenOldestMale(int $total = 10): string
1155    {
1156        $records = $this->topTenOldestQuery('M', $total);
1157
1158        return view(
1159            'statistics/individuals/top10-nolist',
1160            [
1161                'records' => $records,
1162            ]
1163        );
1164    }
1165
1166    /**
1167     * Find the longest lived males.
1168     *
1169     * @param int $total
1170     *
1171     * @return string
1172     */
1173    public function topTenOldestMaleList(int $total = 10): string
1174    {
1175        $records = $this->topTenOldestQuery('M', $total);
1176
1177        return view(
1178            'statistics/individuals/top10-list',
1179            [
1180                'records' => $records,
1181            ]
1182        );
1183    }
1184
1185    /**
1186     * Find the oldest living individuals.
1187     *
1188     * @param string $sex
1189     * @param int    $total
1190     *
1191     * @return array
1192     */
1193    private function topTenOldestAliveQuery(string $sex = 'BOTH', int $total = 10): array
1194    {
1195        if ($sex === 'F') {
1196            $sex_search = " AND i_sex='F'";
1197        } elseif ($sex === 'M') {
1198            $sex_search = " AND i_sex='M'";
1199        } else {
1200            $sex_search = '';
1201        }
1202
1203        $rows = $this->runSql(
1204            "SELECT" .
1205            " birth.d_gid AS id," .
1206            " MIN(birth.d_julianday1) AS age" .
1207            " FROM" .
1208            " `##dates` AS birth," .
1209            " `##individuals` AS indi" .
1210            " WHERE" .
1211            " indi.i_id=birth.d_gid AND" .
1212            " indi.i_gedcom NOT REGEXP '\\n1 (" . implode('|', Gedcom::DEATH_EVENTS) . ")' AND" .
1213            " birth.d_file={$this->tree->id()} AND" .
1214            " birth.d_fact='BIRT' AND" .
1215            " birth.d_file=indi.i_file AND" .
1216            " birth.d_julianday1<>0" .
1217            $sex_search .
1218            " GROUP BY id" .
1219            " ORDER BY age" .
1220            " ASC LIMIT " . $total
1221        );
1222
1223        $top10 = [];
1224
1225        foreach ($rows as $row) {
1226            $person = Individual::getInstance($row->id, $this->tree);
1227
1228            $top10[] = [
1229                'person' => $person,
1230                'age'    => $this->calculateAge(WT_CLIENT_JD - ((int) $row->age)),
1231            ];
1232        }
1233
1234        // TODO
1235//        if (I18N::direction() === 'rtl') {
1236//            $top10 = str_replace([
1237//                '[',
1238//                ']',
1239//                '(',
1240//                ')',
1241//                '+',
1242//            ], [
1243//                '&rlm;[',
1244//                '&rlm;]',
1245//                '&rlm;(',
1246//                '&rlm;)',
1247//                '&rlm;+',
1248//            ], $top10);
1249//        }
1250
1251        return $top10;
1252    }
1253
1254    /**
1255     * Find the oldest living individuals.
1256     *
1257     * @param int $total
1258     *
1259     * @return string
1260     */
1261    public function topTenOldestAlive(int $total = 10): string
1262    {
1263        if (!Auth::isMember($this->tree)) {
1264            return I18N::translate('This information is private and cannot be shown.');
1265        }
1266
1267        $records = $this->topTenOldestAliveQuery('BOTH', $total);
1268
1269        return view(
1270            'statistics/individuals/top10-nolist',
1271            [
1272                'records' => $records,
1273            ]
1274        );
1275    }
1276
1277    /**
1278     * Find the oldest living individuals.
1279     *
1280     * @param int $total
1281     *
1282     * @return string
1283     */
1284    public function topTenOldestListAlive(int $total = 10): string
1285    {
1286        if (!Auth::isMember($this->tree)) {
1287            return I18N::translate('This information is private and cannot be shown.');
1288        }
1289
1290        $records = $this->topTenOldestAliveQuery('BOTH', $total);
1291
1292        return view(
1293            'statistics/individuals/top10-list',
1294            [
1295                'records' => $records,
1296            ]
1297        );
1298    }
1299
1300    /**
1301     * Find the oldest living females.
1302     *
1303     * @param int $total
1304     *
1305     * @return string
1306     */
1307    public function topTenOldestFemaleAlive(int $total = 10): string
1308    {
1309        if (!Auth::isMember($this->tree)) {
1310            return I18N::translate('This information is private and cannot be shown.');
1311        }
1312
1313        $records = $this->topTenOldestAliveQuery('F', $total);
1314
1315        return view(
1316            'statistics/individuals/top10-nolist',
1317            [
1318                'records' => $records,
1319            ]
1320        );
1321    }
1322
1323    /**
1324     * Find the oldest living females.
1325     *
1326     * @param int $total
1327     *
1328     * @return string
1329     */
1330    public function topTenOldestFemaleListAlive(int $total = 10): string
1331    {
1332        if (!Auth::isMember($this->tree)) {
1333            return I18N::translate('This information is private and cannot be shown.');
1334        }
1335
1336        $records = $this->topTenOldestAliveQuery('F', $total);
1337
1338        return view(
1339            'statistics/individuals/top10-list',
1340            [
1341                'records' => $records,
1342            ]
1343        );
1344    }
1345
1346    /**
1347     * Find the longest lived living males.
1348     *
1349     * @param int $total
1350     *
1351     * @return string
1352     */
1353    public function topTenOldestMaleAlive(int $total = 10): string
1354    {
1355        if (!Auth::isMember($this->tree)) {
1356            return I18N::translate('This information is private and cannot be shown.');
1357        }
1358
1359        $records = $this->topTenOldestAliveQuery('M', $total);
1360
1361        return view(
1362            'statistics/individuals/top10-nolist',
1363            [
1364                'records' => $records,
1365            ]
1366        );
1367    }
1368
1369    /**
1370     * Find the longest lived living males.
1371     *
1372     * @param int $total
1373     *
1374     * @return string
1375     */
1376    public function topTenOldestMaleListAlive(int $total = 10): string
1377    {
1378        if (!Auth::isMember($this->tree)) {
1379            return I18N::translate('This information is private and cannot be shown.');
1380        }
1381
1382        $records = $this->topTenOldestAliveQuery('M', $total);
1383
1384        return view(
1385            'statistics/individuals/top10-list',
1386            [
1387                'records' => $records,
1388            ]
1389        );
1390    }
1391
1392    /**
1393     * Find the average lifespan.
1394     *
1395     * @param string $sex
1396     * @param bool   $show_years
1397     *
1398     * @return string
1399     */
1400    private function averageLifespanQuery(string $sex = 'BOTH', bool $show_years = false): string
1401    {
1402        if ($sex === 'F') {
1403            $sex_search = " AND i_sex='F' ";
1404        } elseif ($sex === 'M') {
1405            $sex_search = " AND i_sex='M' ";
1406        } else {
1407            $sex_search = '';
1408        }
1409
1410        $rows = $this->runSql(
1411            "SELECT IFNULL(AVG(death.d_julianday2-birth.d_julianday1), 0) AS age" .
1412            " FROM `##dates` AS death, `##dates` AS birth, `##individuals` AS indi" .
1413            " WHERE " .
1414            " indi.i_id=birth.d_gid AND " .
1415            " birth.d_gid=death.d_gid AND " .
1416            " death.d_file=" . $this->tree->id() . " AND " .
1417            " birth.d_file=death.d_file AND " .
1418            " birth.d_file=indi.i_file AND " .
1419            " birth.d_fact='BIRT' AND " .
1420            " death.d_fact='DEAT' AND " .
1421            " birth.d_julianday1<>0 AND " .
1422            " death.d_julianday1>birth.d_julianday2 " .
1423            $sex_search
1424        );
1425
1426        $age = $rows[0]->age;
1427
1428        if ($show_years) {
1429            return $this->calculateAge((int) $rows[0]->age);
1430        }
1431
1432        return I18N::number($age / 365.25);
1433    }
1434
1435    /**
1436     * Find the average lifespan.
1437     *
1438     * @param bool $show_years
1439     *
1440     * @return string
1441     */
1442    public function averageLifespan($show_years = false): string
1443    {
1444        return $this->averageLifespanQuery('BOTH', $show_years);
1445    }
1446
1447    /**
1448     * Find the average lifespan of females.
1449     *
1450     * @param bool $show_years
1451     *
1452     * @return string
1453     */
1454    public function averageLifespanFemale($show_years = false): string
1455    {
1456        return $this->averageLifespanQuery('F', $show_years);
1457    }
1458
1459    /**
1460     * Find the average male lifespan.
1461     *
1462     * @param bool $show_years
1463     *
1464     * @return string
1465     */
1466    public function averageLifespanMale($show_years = false): string
1467    {
1468        return $this->averageLifespanQuery('M', $show_years);
1469    }
1470
1471    /**
1472     * Convert totals into percentages.
1473     *
1474     * @param int $count
1475     * @param int $total
1476     *
1477     * @return string
1478     */
1479    private function getPercentage(int $count, int $total): string
1480    {
1481        return I18N::percentage($count / $total, 1);
1482    }
1483
1484    /**
1485     * Returns how many individuals exist in the tree.
1486     *
1487     * @return int
1488     */
1489    private function totalIndividualsQuery(): int
1490    {
1491        return DB::table('individuals')
1492            ->where('i_file', '=', $this->tree->id())
1493            ->count();
1494    }
1495
1496    /**
1497     * Count the number of living individuals.
1498     *
1499     * The totalLiving/totalDeceased queries assume that every dead person will
1500     * have a DEAT record. It will not include individuals who were born more
1501     * than MAX_ALIVE_AGE years ago, and who have no DEAT record.
1502     * A good reason to run the “Add missing DEAT records” batch-update!
1503     *
1504     * @return int
1505     */
1506    private function totalLivingQuery(): int
1507    {
1508        return DB::table('individuals')
1509            ->where('i_file', '=', $this->tree->id())
1510            ->where(
1511                'i_gedcom',
1512                'not regexp',
1513                "\n1 (" . implode('|', Gedcom::DEATH_EVENTS) . ')'
1514            )
1515            ->count();
1516    }
1517
1518    /**
1519     * Count the number of dead individuals.
1520     *
1521     * @return int
1522     */
1523    private function totalDeceasedQuery(): int
1524    {
1525        return DB::table('individuals')
1526            ->where('i_file', '=', $this->tree->id())
1527            ->where(
1528                'i_gedcom',
1529                'regexp',
1530                "\n1 (" . implode('|', Gedcom::DEATH_EVENTS) . ')'
1531            )
1532            ->count();
1533    }
1534
1535    /**
1536     * Returns the total count of a specific sex.
1537     *
1538     * @param string $sex The sex to query
1539     *
1540     * @return int
1541     */
1542    private function getTotalSexQuery(string $sex): int
1543    {
1544        return DB::table('individuals')
1545            ->where('i_file', '=', $this->tree->id())
1546            ->where('i_sex', '=', $sex)
1547            ->count();
1548    }
1549
1550    /**
1551     * Returns the total number of males.
1552     *
1553     * @return int
1554     */
1555    private function totalSexMalesQuery(): int
1556    {
1557        return $this->getTotalSexQuery('M');
1558    }
1559
1560    /**
1561     * Returns the total number of females.
1562     *
1563     * @return int
1564     */
1565    private function totalSexFemalesQuery(): int
1566    {
1567        return $this->getTotalSexQuery('F');
1568    }
1569
1570    /**
1571     * Returns the total number of individuals with unknown sex.
1572     *
1573     * @return int
1574     */
1575    private function totalSexUnknownQuery(): int
1576    {
1577        return $this->getTotalSexQuery('U');
1578    }
1579
1580    /**
1581     * Count the total families.
1582     *
1583     * @return int
1584     */
1585    private function totalFamiliesQuery(): int
1586    {
1587        return DB::table('families')
1588            ->where('f_file', '=', $this->tree->id())
1589            ->count();
1590    }
1591
1592    /**
1593     * How many individuals have one or more sources.
1594     *
1595     * @return int
1596     */
1597    private function totalIndisWithSourcesQuery(): int
1598    {
1599        return DB::table('individuals')
1600            ->select(['i_id'])
1601            ->distinct()
1602            ->join('link', function (JoinClause $join) {
1603                $join->on('i_id', '=', 'l_from')
1604                    ->on('i_file', '=', 'l_file');
1605            })
1606            ->where('l_file', '=', $this->tree->id())
1607            ->where('l_type', '=', 'SOUR')
1608            ->count('i_id');
1609    }
1610
1611    /**
1612     * Count the families with source records.
1613     *
1614     * @return int
1615     */
1616    private function totalFamsWithSourcesQuery(): int
1617    {
1618        return DB::table('families')
1619            ->select(['f_id'])
1620            ->distinct()
1621            ->join('link', function (JoinClause $join) {
1622                $join->on('f_id', '=', 'l_from')
1623                    ->on('f_file', '=', 'l_file');
1624            })
1625            ->where('l_file', '=', $this->tree->id())
1626            ->where('l_type', '=', 'SOUR')
1627            ->count('f_id');
1628    }
1629
1630    /**
1631     * Count the number of repositories.
1632     *
1633     * @return int
1634     */
1635    private function totalRepositoriesQuery(): int
1636    {
1637        return DB::table('other')
1638            ->where('o_file', '=', $this->tree->id())
1639            ->where('o_type', '=', 'REPO')
1640            ->count();
1641    }
1642
1643    /**
1644     * Count the total number of sources.
1645     *
1646     * @return int
1647     */
1648    private function totalSourcesQuery(): int
1649    {
1650        return DB::table('sources')
1651            ->where('s_file', '=', $this->tree->id())
1652            ->count();
1653    }
1654
1655    /**
1656     * Count the number of notes.
1657     *
1658     * @return int
1659     */
1660    private function totalNotesQuery(): int
1661    {
1662        return DB::table('other')
1663            ->where('o_file', '=', $this->tree->id())
1664            ->where('o_type', '=', 'NOTE')
1665            ->count();
1666    }
1667
1668    /**
1669     * Returns the total number of records.
1670     *
1671     * @return int
1672     */
1673    private function totalRecordsQuery(): int
1674    {
1675        return $this->totalIndividualsQuery()
1676            + $this->totalFamiliesQuery()
1677            + $this->totalNotesQuery()
1678            + $this->totalRepositoriesQuery()
1679            + $this->totalSourcesQuery();
1680    }
1681
1682    /**
1683     * @inheritDoc
1684     */
1685    public function totalRecords(): string
1686    {
1687        return I18N::number($this->totalRecordsQuery());
1688    }
1689
1690    /**
1691     * @inheritDoc
1692     */
1693    public function totalIndividuals(): string
1694    {
1695        return I18N::number($this->totalIndividualsQuery());
1696    }
1697
1698    /**
1699     * Count the number of living individuals.
1700     *
1701     * @return string
1702     */
1703    public function totalLiving(): string
1704    {
1705        return I18N::number($this->totalLivingQuery());
1706    }
1707
1708    /**
1709     * Count the number of dead individuals.
1710     *
1711     * @return string
1712     */
1713    public function totalDeceased(): string
1714    {
1715        return I18N::number($this->totalDeceasedQuery());
1716    }
1717
1718    /**
1719     * @inheritDoc
1720     */
1721    public function totalSexMales(): string
1722    {
1723        return I18N::number($this->totalSexMalesQuery());
1724    }
1725
1726    /**
1727     * @inheritDoc
1728     */
1729    public function totalSexFemales(): string
1730    {
1731        return I18N::number($this->totalSexFemalesQuery());
1732    }
1733
1734    /**
1735     * @inheritDoc
1736     */
1737    public function totalSexUnknown(): string
1738    {
1739        return I18N::number($this->totalSexUnknownQuery());
1740    }
1741
1742    /**
1743     * @inheritDoc
1744     */
1745    public function totalFamilies(): string
1746    {
1747        return I18N::number($this->totalFamiliesQuery());
1748    }
1749
1750    /**
1751     * How many individuals have one or more sources.
1752     *
1753     * @return string
1754     */
1755    public function totalIndisWithSources(): string
1756    {
1757        return I18N::number($this->totalIndisWithSourcesQuery());
1758    }
1759
1760    /**
1761     * Count the families with with source records.
1762     *
1763     * @return string
1764     */
1765    public function totalFamsWithSources(): string
1766    {
1767        return I18N::number($this->totalFamsWithSourcesQuery());
1768    }
1769
1770    /**
1771     * @inheritDoc
1772     */
1773    public function totalRepositories(): string
1774    {
1775        return I18N::number($this->totalRepositoriesQuery());
1776    }
1777
1778    /**
1779     * @inheritDoc
1780     */
1781    public function totalSources(): string
1782    {
1783        return I18N::number($this->totalSourcesQuery());
1784    }
1785
1786    /**
1787     * @inheritDoc
1788     */
1789    public function totalNotes(): string
1790    {
1791        return I18N::number($this->totalNotesQuery());
1792    }
1793
1794    /**
1795     * @inheritDoc
1796     */
1797    public function totalIndividualsPercentage(): string
1798    {
1799        return $this->getPercentage(
1800            $this->totalIndividualsQuery(),
1801            $this->totalRecordsQuery()
1802        );
1803    }
1804
1805    /**
1806     * @inheritDoc
1807     */
1808    public function totalFamiliesPercentage(): string
1809    {
1810        return $this->getPercentage(
1811            $this->totalFamiliesQuery(),
1812            $this->totalRecordsQuery()
1813        );
1814    }
1815
1816    /**
1817     * @inheritDoc
1818     */
1819    public function totalRepositoriesPercentage(): string
1820    {
1821        return $this->getPercentage(
1822            $this->totalRepositoriesQuery(),
1823            $this->totalRecordsQuery()
1824        );
1825    }
1826
1827    /**
1828     * @inheritDoc
1829     */
1830    public function totalSourcesPercentage(): string
1831    {
1832        return $this->getPercentage(
1833            $this->totalSourcesQuery(),
1834            $this->totalRecordsQuery()
1835        );
1836    }
1837
1838    /**
1839     * @inheritDoc
1840     */
1841    public function totalNotesPercentage(): string
1842    {
1843        return $this->getPercentage(
1844            $this->totalNotesQuery(),
1845            $this->totalRecordsQuery()
1846        );
1847    }
1848
1849    /**
1850     * @inheritDoc
1851     */
1852    public function totalLivingPercentage(): string
1853    {
1854        return $this->getPercentage(
1855            $this->totalLivingQuery(),
1856            $this->totalIndividualsQuery()
1857        );
1858    }
1859
1860    /**
1861     * @inheritDoc
1862     */
1863    public function totalDeceasedPercentage(): string
1864    {
1865        return $this->getPercentage(
1866            $this->totalDeceasedQuery(),
1867            $this->totalIndividualsQuery()
1868        );
1869    }
1870
1871    /**
1872     * @inheritDoc
1873     */
1874    public function totalSexMalesPercentage(): string
1875    {
1876        return $this->getPercentage(
1877            $this->totalSexMalesQuery(),
1878            $this->totalIndividualsQuery()
1879        );
1880    }
1881
1882    /**
1883     * @inheritDoc
1884     */
1885    public function totalSexFemalesPercentage(): string
1886    {
1887        return $this->getPercentage(
1888            $this->totalSexFemalesQuery(),
1889            $this->totalIndividualsQuery()
1890        );
1891    }
1892
1893    /**
1894     * @inheritDoc
1895     */
1896    public function totalSexUnknownPercentage(): string
1897    {
1898        return $this->getPercentage(
1899            $this->totalSexUnknownQuery(),
1900            $this->totalIndividualsQuery()
1901        );
1902    }
1903
1904    /**
1905     * Create a chart of common given names.
1906     *
1907     * @param string|null $size
1908     * @param string|null $color_from
1909     * @param string|null $color_to
1910     * @param int         $maxtoshow
1911     *
1912     * @return string
1913     */
1914    public function chartCommonGiven(
1915        string $size = null,
1916        string $color_from = null,
1917        string $color_to = null,
1918        int $maxtoshow = 7
1919    ): string {
1920        $tot_indi = $this->totalIndividualsQuery();
1921        $given    = $this->commonGivenQuery('B', 'chart', false, 1, $maxtoshow);
1922
1923        return (new ChartCommonGiven())
1924            ->chartCommonGiven($tot_indi, $given, $size, $color_from, $color_to);
1925    }
1926
1927    /**
1928     * Create a chart of common surnames.
1929     *
1930     * @param string|null $size
1931     * @param string|null $color_from
1932     * @param string|null $color_to
1933     * @param int         $number_of_surnames
1934     *
1935     * @return string
1936     */
1937    public function chartCommonSurnames(
1938        string $size = null,
1939        string $color_from = null,
1940        string $color_to = null,
1941        int $number_of_surnames = 10
1942    ): string {
1943        $tot_indi     = $this->totalIndividualsQuery();
1944        $all_surnames = $this->topSurnames($number_of_surnames, 0);
1945
1946        return (new ChartCommonSurname($this->tree))
1947            ->chartCommonSurnames($tot_indi, $all_surnames, $size, $color_from, $color_to);
1948    }
1949
1950    /**
1951     * Create a chart showing mortality.
1952     *
1953     * @param string|null $size
1954     * @param string|null $color_living
1955     * @param string|null $color_dead
1956     *
1957     * @return string
1958     */
1959    public function chartMortality(string $size = null, string $color_living = null, string $color_dead = null): string
1960    {
1961        $tot_l = $this->totalLivingQuery();
1962        $tot_d = $this->totalDeceasedQuery();
1963
1964        return (new ChartMortality($this->tree))
1965            ->chartMortality($tot_l, $tot_d, $size, $color_living, $color_dead);
1966    }
1967
1968    /**
1969     * Create a chart showing individuals with/without sources.
1970     *
1971     * @param string|null $size
1972     * @param string|null $color_from
1973     * @param string|null $color_to
1974     *
1975     * @return string
1976     */
1977    public function chartIndisWithSources(
1978        string $size       = null,
1979        string $color_from = null,
1980        string $color_to   = null
1981    ): string {
1982        $tot_indi        = $this->totalIndividualsQuery();
1983        $tot_indi_source = $this->totalIndisWithSourcesQuery();
1984
1985        return (new ChartIndividual())
1986            ->chartIndisWithSources($tot_indi, $tot_indi_source, $size, $color_from, $color_to);
1987    }
1988
1989    /**
1990     * Create a chart of individuals with/without sources.
1991     *
1992     * @param string|null $size
1993     * @param string|null $color_from
1994     * @param string|null $color_to
1995     *
1996     * @return string
1997     */
1998    public function chartFamsWithSources(
1999        string $size       = null,
2000        string $color_from = null,
2001        string $color_to   = null
2002    ): string {
2003        $tot_fam        = $this->totalFamiliesQuery();
2004        $tot_fam_source = $this->totalFamsWithSourcesQuery();
2005
2006        return (new ChartFamilyWithSources())
2007            ->chartFamsWithSources($tot_fam, $tot_fam_source, $size, $color_from, $color_to);
2008    }
2009
2010    /**
2011     * @inheritDoc
2012     */
2013    public function chartSex(
2014        string $size          = null,
2015        string $color_female  = null,
2016        string $color_male    = null,
2017        string $color_unknown = null
2018    ): string {
2019        $tot_m = $this->totalSexMalesQuery();
2020        $tot_f = $this->totalSexFemalesQuery();
2021        $tot_u = $this->totalSexUnknownQuery();
2022
2023        return (new ChartSex($this->tree))
2024            ->chartSex($tot_m, $tot_f, $tot_u, $size, $color_female, $color_male, $color_unknown);
2025    }
2026}
2027