Stats::usersLoggedInQuery()   F
last analyzed

Complexity

Conditions 19
Paths 171

Size

Total Lines 59
Code Lines 40

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 19
eloc 40
nc 171
nop 1
dl 0
loc 59
rs 3.925
c 0
b 0
f 0

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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
 */
16
namespace Fisharebest\Webtrees;
17
18
use Fisharebest\Webtrees\Functions\Functions;
19
use Fisharebest\Webtrees\Functions\FunctionsDate;
20
use Fisharebest\Webtrees\Functions\FunctionsDb;
21
use Fisharebest\Webtrees\Functions\FunctionsPrint;
22
use Fisharebest\Webtrees\Functions\FunctionsPrintLists;
23
use Fisharebest\Webtrees\Module\FamilyTreeFavoritesModule;
24
use Fisharebest\Webtrees\Module\UserFavoritesModule;
25
use Fisharebest\Webtrees\Query\QueryName;
26
use PDO;
27
use PDOException;
28
use Rhumsaa\Uuid\Uuid;
29
30
/**
31
 * A selection of pre-formatted statistical queries.
32
 *
33
 * These are primarily used for embedded keywords on HTML blocks, but
34
 * are also used elsewhere in the code.
35
 */
36
class Stats
37
{
38
    /** @var Tree Generate statistics for a specified tree. */
39
    private $tree;
40
41
    /** @var string[] All public functions are available as keywords - except these ones */
42
    private $public_but_not_allowed = array(
43
        '__construct', 'embedTags', 'iso3166', 'getAllCountries', 'getAllTagsTable', 'getAllTagsText', 'statsPlaces', 'statsBirthQuery', 'statsDeathQuery', 'statsMarrQuery', 'statsAgeQuery', 'monthFirstChildQuery', 'statsChildrenQuery', 'statsMarrAgeQuery',
44
    );
45
46
    /** @var string[] List of GEDCOM media types */
47
    private $_media_types = array('audio', 'book', 'card', 'certificate', 'coat', 'document', 'electronic', 'magazine', 'manuscript', 'map', 'fiche', 'film', 'newspaper', 'painting', 'photo', 'tombstone', 'video', 'other');
48
49
    /**
50
     * Create the statistics for a tree.
51
     *
52
     * @param Tree $tree Generate statistics for this tree
53
     */
54
    public function __construct(Tree $tree)
55
    {
56
        $this->tree = $tree;
57
    }
58
59
    /**
60
     * Return a string of all supported tags and an example of its output in table row form.
61
     *
62
     * @return string
63
     */
64
    public function getAllTagsTable()
65
    {
66
        $examples = array();
67
        foreach (get_class_methods($this) as $method) {
68
            $reflection = new \ReflectionMethod($this, $method);
69
            if ($reflection->isPublic() && !in_array($method, $this->public_but_not_allowed)) {
70
                $examples[$method] = $this->$method();
71
            }
72
        }
73
        ksort($examples);
74
75
        $html = '';
76
        foreach ($examples as $tag => $value) {
77
            $html .= '<tr>';
78
            $html .= '<td class="list_value_wrap">' . $tag . '</td>';
79
            $html .= '<td class="list_value_wrap">' . $value . '</td>';
80
            $html .= '</tr>';
81
        }
82
83
        return
84
            '<table id="keywords" style="width:100%; table-layout:fixed"><thead>' .
85
            '<tr>' .
86
            '<th class="list_label_wrap width25">' .
87
            I18N::translate('Embedded variable') .
88
            '</th>' .
89
            '<th class="list_label_wrap width75">' .
90
            I18N::translate('Resulting value') .
91
            '</th>' .
92
            '</tr>' .
93
            '</thead><tbody>' .
94
            $html .
95
            '</tbody></table>';
96
    }
97
98
    /**
99
     * Return a string of all supported tags in plain text.
100
     *
101
     * @return string
102
     */
103
    public function getAllTagsText()
104
    {
105
        $examples = array();
106
        foreach (get_class_methods($this) as $method) {
107
            $reflection = new \ReflectionMethod($this, $method);
108
            if ($reflection->isPublic() && !in_array($method, $this->public_but_not_allowed)) {
109
                $examples[$method] = $method;
110
            }
111
        }
112
        ksort($examples);
113
114
        return implode('<br>', $examples);
115
    }
116
117
    /**
118
     * Get tags and their parsed results.
119
     *
120
     * @param string $text
121
     *
122
     * @return string[][]
123
     */
124
    private function getTags($text)
125
    {
126
        static $funcs;
127
128
        // Retrive all class methods
129
        isset($funcs) or $funcs = get_class_methods($this);
130
131
        // Extract all tags from the provided text
132
        preg_match_all("/#([^#]+)(?=#)/", (string) $text, $match);
133
        $tags       = $match[1];
134
        $c          = count($tags);
135
        $new_tags   = array(); // tag to replace
136
        $new_values = array(); // value to replace it with
137
138
        /*
139
         * Parse block tags.
140
         */
141
        for ($i = 0; $i < $c; $i++) {
142
            $full_tag = $tags[$i];
143
            // Added for new parameter support
144
            $params = explode(':', $tags[$i]);
145
            if (count($params) > 1) {
146
                $tags[$i] = array_shift($params);
147
            } else {
148
                $params = array();
149
            }
150
151
            // Generate the replacement value for the tag
152
            if (method_exists($this, $tags[$i])) {
153
                $new_tags[]   = "#{$full_tag}#";
154
                $new_values[] = call_user_func_array(array($this, $tags[$i]), array($params));
155
            }
156
        }
157
158
        return array($new_tags, $new_values);
159
    }
160
161
    /**
162
     * Embed tags in text
163
     *
164
     * @param string $text
165
     *
166
     * @return string
167
     */
168
    public function embedTags($text)
169
    {
170
        if (strpos($text, '#') !== false) {
171
            list($new_tags, $new_values) = $this->getTags($text);
172
            $text                        = str_replace($new_tags, $new_values, $text);
173
        }
174
175
        return $text;
176
    }
177
178
    /**
179
     * Get the name used for GEDCOM files and URLs.
180
     *
181
     * @return string
182
     */
183
    public function gedcomFilename()
184
    {
185
        return $this->tree->getName();
186
    }
187
188
    /**
189
     * Get the internal ID number of the tree.
190
     *
191
     * @return int
192
     */
193
    public function gedcomId()
194
    {
195
        return $this->tree->getTreeId();
196
    }
197
198
    /**
199
     * Get the descriptive title of the tree.
200
     *
201
     * @return string
202
     */
203
    public function gedcomTitle()
204
    {
205
        return $this->tree->getTitleHtml();
206
    }
207
208
    /**
209
     * Get information from the GEDCOM's HEAD record.
210
     *
211
     * @return string[]
212
     */
213
    private function gedcomHead()
214
    {
215
        $title   = '';
216
        $version = '';
217
        $source  = '';
218
219
        $head = GedcomRecord::getInstance('HEAD', $this->tree);
220
        $sour = $head->getFirstFact('SOUR');
221
        if ($sour) {
222
            $source  = $sour->getValue();
223
            $title   = $sour->getAttribute('NAME');
224
            $version = $sour->getAttribute('VERS');
225
        }
226
227
        return array($title, $version, $source);
228
    }
229
230
    /**
231
     * Get the software originally used to create the GEDCOM file.
232
     *
233
     * @return string
234
     */
235
    public function gedcomCreatedSoftware()
236
    {
237
        $head = $this->gedcomHead();
238
239
        return $head[0];
240
    }
241
242
    /**
243
     * Get the version of software which created the GEDCOM file.
244
     *
245
     * @return string
246
     */
247
    public function gedcomCreatedVersion()
248
    {
249
        $head = $this->gedcomHead();
250
        // fix broken version string in Family Tree Maker
251
        if (strstr($head[1], 'Family Tree Maker ')) {
252
            $p       = strpos($head[1], '(') + 1;
253
            $p2      = strpos($head[1], ')');
254
            $head[1] = substr($head[1], $p, ($p2 - $p));
255
        }
256
        // Fix EasyTree version
257
        if ($head[2] == 'EasyTree') {
258
            $head[1] = substr($head[1], 1);
259
        }
260
261
        return $head[1];
262
    }
263
264
    /**
265
     * Get the date the GEDCOM file was created.
266
     *
267
     * @return string
268
     */
269
    public function gedcomDate()
270
    {
271
        $head = GedcomRecord::getInstance('HEAD', $this->tree);
272
        $fact = $head->getFirstFact('DATE');
273
        if ($fact) {
274
            $date = new Date($fact->getValue());
275
276
            return $date->display();
277
        }
278
279
        return '';
280
    }
281
282
    /**
283
     * When was this tree last updated?
284
     *
285
     * @return string
286
     */
287
    public function gedcomUpdated()
288
    {
289
        $row = Database::prepare(
290
            "SELECT d_year, d_month, d_day FROM `##dates` WHERE d_julianday1 = (SELECT MAX(d_julianday1) FROM `##dates` WHERE d_file =? AND d_fact='CHAN') LIMIT 1"
291
        )->execute(array($this->tree->getTreeId()))->fetchOneRow();
292
        if ($row) {
293
            $date = new Date("{$row->d_day} {$row->d_month} {$row->d_year}");
294
295
            return $date->display();
296
        } else {
297
            return $this->gedcomDate();
298
        }
299
    }
300
301
    /**
302
     * What is the significant individual from this tree?
303
     *
304
     * @return string
305
     */
306
    public function gedcomRootId()
307
    {
308
        return $this->tree->getPreference('PEDIGREE_ROOT_ID');
309
    }
310
311
    /**
312
     * Convert totals into percentages.
313
     *
314
     * @param string $total
315
     * @param string $type
316
     *
317
     * @return string
318
     */
319
    private function getPercentage($total, $type)
320
    {
321
        switch ($type) {
322
            case 'individual':
323
                $type = $this->totalIndividualsQuery();
324
                break;
325
            case 'family':
326
                $type = $this->totalFamiliesQuery();
327
                break;
328
            case 'source':
329
                $type = $this->totalSourcesQuery();
330
                break;
331
            case 'note':
332
                $type = $this->totalNotesQuery();
333
                break;
334
            case 'all':
335
            default:
336
                $type = $this->totalIndividualsQuery() + $this->totalFamiliesQuery() + $this->totalSourcesQuery();
337
                break;
338
        }
339
        if ($type == 0) {
340
            return I18N::percentage(0, 1);
341
        } else {
342
            return I18N::percentage($total / $type, 1);
343
        }
344
    }
345
346
    /**
347
     * How many GEDCOM records exist in the tree.
348
     *
349
     * @return string
350
     */
351
    public function totalRecords()
352
    {
353
        return I18N::number($this->totalIndividualsQuery() + $this->totalFamiliesQuery() + $this->totalSourcesQuery());
354
    }
355
356
    /**
357
     * How many individuals exist in the tree.
358
     *
359
     * @return int
360
     */
361
    private function totalIndividualsQuery()
362
    {
363
        return (int) Database::prepare(
364
            "SELECT COUNT(*) FROM `##individuals` WHERE i_file = :tree_id"
365
        )->execute(array(
366
            'tree_id' => $this->tree->getTreeId(),
367
        ))->fetchOne();
368
    }
369
370
    /**
371
     * How many individuals exist in the tree.
372
     *
373
     * @return string
374
     */
375
    public function totalIndividuals()
376
    {
377
        return I18N::number($this->totalIndividualsQuery());
378
    }
379
380
    /**
381
     * How many individuals have one or more sources.
382
     *
383
     * @return int
384
     */
385
    private function totalIndisWithSourcesQuery()
386
    {
387
        return (int) Database::prepare(
388
            "SELECT COUNT(DISTINCT i_id)" .
389
            " FROM `##individuals` JOIN `##link` ON i_id = l_from AND i_file = l_file" .
390
            " WHERE l_file = :tree_id AND l_type = 'SOUR'"
391
        )->execute(array(
392
            'tree_id' => $this->tree->getTreeId(),
393
        ))->fetchOne();
394
    }
395
396
    /**
397
     * How many individuals have one or more sources.
398
     *
399
     * @return string
400
     */
401
    public function totalIndisWithSources()
402
    {
403
        return I18N::number($this->totalIndisWithSourcesQuery());
404
    }
405
406
    /**
407
     * Create a chart showing individuals with/without sources.
408
     *
409
     * @param string[] $params
410
     *
411
     * @return string
412
     */
413
    public function chartIndisWithSources($params = array())
414
    {
415
        $WT_STATS_CHART_COLOR1 = Theme::theme()->parameter('distribution-chart-no-values');
416
        $WT_STATS_CHART_COLOR2 = Theme::theme()->parameter('distribution-chart-high-values');
417
        $WT_STATS_S_CHART_X    = Theme::theme()->parameter('stats-small-chart-x');
418
        $WT_STATS_S_CHART_Y    = Theme::theme()->parameter('stats-small-chart-y');
419
420
        if (isset($params[0]) && $params[0] != '') {
421
            $size = strtolower($params[0]);
422
        } else {
423
            $size = $WT_STATS_S_CHART_X . 'x' . $WT_STATS_S_CHART_Y;
424
        }
425
        if (isset($params[1]) && $params[1] != '') {
426
            $color_from = strtolower($params[1]);
427
        } else {
428
            $color_from = $WT_STATS_CHART_COLOR1;
429
        }
430
        if (isset($params[2]) && $params[2] != '') {
431
            $color_to = strtolower($params[2]);
432
        } else {
433
            $color_to = $WT_STATS_CHART_COLOR2;
434
        }
435
        $sizes    = explode('x', $size);
436
        $tot_indi = $this->totalIndividualsQuery();
437
        if ($tot_indi == 0) {
438
            return '';
439
        } else {
440
            $tot_sindi_per = round($this->totalIndisWithSourcesQuery() / $tot_indi, 3);
441
            $chd           = $this->arrayToExtendedEncoding(array(100 - 100 * $tot_sindi_per, 100 * $tot_sindi_per));
0 ignored issues
show
Bug introduced by
array(100 - 100 * $tot_s..., 100 * $tot_sindi_per) of type array<integer,double> is incompatible with the type integer[] expected by parameter $a of Fisharebest\Webtrees\Sta...rayToExtendedEncoding(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

441
            $chd           = $this->arrayToExtendedEncoding(/** @scrutinizer ignore-type */ array(100 - 100 * $tot_sindi_per, 100 * $tot_sindi_per));
Loading history...
442
            $chl           = I18N::translate('Without sources') . ' - ' . I18N::percentage(1 - $tot_sindi_per, 1) . '|' .
443
                I18N::translate('With sources') . ' - ' . I18N::percentage($tot_sindi_per, 1);
444
            $chart_title = I18N::translate('Individuals with sources');
445
446
            return '<img src="https://chart.googleapis.com/chart?cht=p3&amp;chd=e:' . $chd . '&amp;chs=' . $size . '&amp;chco=' . $color_from . ',' . $color_to . '&amp;chf=bg,s,ffffff00&amp;chl=' . rawurlencode($chl) . '" width="' . $sizes[0] . '" height="' . $sizes[1] . '" alt="' . $chart_title . '" title="' . $chart_title . '">';
447
        }
448
    }
449
450
    /**
451
     * Show the total individuals as a percentage.
452
     *
453
     * @return string
454
     */
455
    public function totalIndividualsPercentage()
456
    {
457
        return $this->getPercentage($this->totalIndividualsQuery(), 'all');
458
    }
459
460
    /**
461
     * Count the total families.
462
     *
463
     * @return int
464
     */
465
    private function totalFamiliesQuery()
466
    {
467
        return (int) Database::prepare(
468
            "SELECT COUNT(*) FROM `##families` WHERE f_file = :tree_id"
469
        )->execute(array(
470
            'tree_id' => $this->tree->getTreeId(),
471
        ))->fetchOne();
472
    }
473
474
    /**
475
     * Count the total families.
476
     *
477
     * @return string
478
     */
479
    public function totalFamilies()
480
    {
481
        return I18N::number($this->totalFamiliesQuery());
482
    }
483
484
    /**
485
     * Count the families with source records.
486
     *
487
     * @return int
488
     */
489
    private function totalFamsWithSourcesQuery()
490
    {
491
        return (int) Database::prepare(
492
            "SELECT COUNT(DISTINCT f_id)" .
493
            " FROM `##families` JOIN `##link` ON f_id = l_from AND f_file = l_file" .
494
            " WHERE l_file = :tree_id AND l_type = 'SOUR'"
495
        )->execute(array(
496
            'tree_id' => $this->tree->getTreeId(),
497
        ))->fetchOne();
498
    }
499
500
    /**
501
     * Count the families with with source records.
502
     *
503
     * @return string
504
     */
505
    public function totalFamsWithSources()
506
    {
507
        return I18N::number($this->totalFamsWithSourcesQuery());
508
    }
509
510
    /**
511
     * Create a chart of individuals with/without sources.
512
     *
513
     * @param string[] $params
514
     *
515
     * @return string
516
     */
517
    public function chartFamsWithSources($params = array())
518
    {
519
        $WT_STATS_CHART_COLOR1 = Theme::theme()->parameter('distribution-chart-no-values');
520
        $WT_STATS_CHART_COLOR2 = Theme::theme()->parameter('distribution-chart-high-values');
521
        $WT_STATS_S_CHART_X    = Theme::theme()->parameter('stats-small-chart-x');
522
        $WT_STATS_S_CHART_Y    = Theme::theme()->parameter('stats-small-chart-y');
523
524
        if (isset($params[0]) && $params[0] != '') {
525
            $size = strtolower($params[0]);
526
        } else {
527
            $size = $WT_STATS_S_CHART_X . "x" . $WT_STATS_S_CHART_Y;
528
        }
529
        if (isset($params[1]) && $params[1] != '') {
530
            $color_from = strtolower($params[1]);
531
        } else {
532
            $color_from = $WT_STATS_CHART_COLOR1;
533
        }
534
        if (isset($params[2]) && $params[2] != '') {
535
            $color_to = strtolower($params[2]);
536
        } else {
537
            $color_to = $WT_STATS_CHART_COLOR2;
538
        }
539
        $sizes   = explode('x', $size);
540
        $tot_fam = $this->totalFamiliesQuery();
541
        if ($tot_fam == 0) {
542
            return '';
543
        } else {
544
            $tot_sfam_per = round($this->totalFamsWithSourcesQuery() / $tot_fam, 3);
545
            $chd          = $this->arrayToExtendedEncoding(array(100 - 100 * $tot_sfam_per, 100 * $tot_sfam_per));
0 ignored issues
show
Bug introduced by
array(100 - 100 * $tot_s...r, 100 * $tot_sfam_per) of type array<integer,double> is incompatible with the type integer[] expected by parameter $a of Fisharebest\Webtrees\Sta...rayToExtendedEncoding(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

545
            $chd          = $this->arrayToExtendedEncoding(/** @scrutinizer ignore-type */ array(100 - 100 * $tot_sfam_per, 100 * $tot_sfam_per));
Loading history...
546
            $chl          = I18N::translate('Without sources') . ' - ' . I18N::percentage(1 - $tot_sfam_per, 1) . '|' .
547
                I18N::translate('With sources') . ' - ' . I18N::percentage($tot_sfam_per, 1);
548
            $chart_title = I18N::translate('Families with sources');
549
550
            return "<img src=\"https://chart.googleapis.com/chart?cht=p3&amp;chd=e:{$chd}&chs={$size}&amp;chco={$color_from},{$color_to}&amp;chf=bg,s,ffffff00&amp;chl={$chl}\" width=\"{$sizes[0]}\" height=\"{$sizes[1]}\" alt=\"" . $chart_title . "\" title=\"" . $chart_title . "\" />";
551
        }
552
    }
553
554
    /**
555
     * Show the total families as a percentage.
556
     *
557
     * @return string
558
     */
559
    public function totalFamiliesPercentage()
560
    {
561
        return $this->getPercentage($this->totalFamiliesQuery(), 'all');
562
    }
563
564
    /**
565
     * Count the total number of sources.
566
     *
567
     * @return int
568
     */
569
    private function totalSourcesQuery()
570
    {
571
        return (int) Database::prepare(
572
            "SELECT COUNT(*) FROM `##sources` WHERE s_file = :tree_id"
573
        )->execute(array(
574
            'tree_id' => $this->tree->getTreeId(),
575
        ))->fetchOne();
576
    }
577
578
    /**
579
     * Count the total number of sources.
580
     *
581
     * @return string
582
     */
583
    public function totalSources()
584
    {
585
        return I18N::number($this->totalSourcesQuery());
586
    }
587
588
    /**
589
     * Show the number of sources as a percentage.
590
     *
591
     * @return string
592
     */
593
    public function totalSourcesPercentage()
594
    {
595
        return $this->getPercentage($this->totalSourcesQuery(), 'all');
596
    }
597
598
    /**
599
     * Count the number of notes.
600
     *
601
     * @return int
602
     */
603
    private function totalNotesQuery()
604
    {
605
        return (int) Database::prepare(
606
            "SELECT COUNT(*) FROM `##other` WHERE o_type='NOTE' AND o_file = :tree_id"
607
        )->execute(array(
608
            'tree_id' => $this->tree->getTreeId(),
609
        ))->fetchOne();
610
    }
611
612
    /**
613
     * Count the number of notes.
614
     *
615
     * @return string
616
     */
617
    public function totalNotes()
618
    {
619
        return I18N::number($this->totalNotesQuery());
620
    }
621
622
    /**
623
     * Show the number of notes as a percentage.
624
     *
625
     * @return string
626
     */
627
    public function totalNotesPercentage()
628
    {
629
        return $this->getPercentage($this->totalNotesQuery(), 'all');
630
    }
631
632
    /**
633
     * Count the number of repositories.
634
     *
635
     * @return int
636
     */
637
    private function totalRepositoriesQuery()
638
    {
639
        return (int) Database::prepare(
640
            "SELECT COUNT(*) FROM `##other` WHERE o_type='REPO' AND o_file = :tree_id"
641
        )->execute(array(
642
            'tree_id' => $this->tree->getTreeId(),
643
        ))->fetchOne();
644
    }
645
646
    /**
647
     * Count the number of repositories
648
     *
649
     * @return string
650
     */
651
    public function totalRepositories()
652
    {
653
        return I18N::number($this->totalRepositoriesQuery());
654
    }
655
656
    /**
657
     * Show the total number of repositories as a percentage.
658
     *
659
     * @return string
660
     */
661
    public function totalRepositoriesPercentage()
662
    {
663
        return $this->getPercentage($this->totalRepositoriesQuery(), 'all');
664
    }
665
666
    /**
667
     * Count the surnames.
668
     *
669
     * @param string[] $params
670
     *
671
     * @return string
672
     */
673
    public function totalSurnames($params = array())
674
    {
675
        if ($params) {
676
            $opt      = 'IN (' . implode(',', array_fill(0, count($params), '?')) . ')';
677
            $distinct = '';
678
        } else {
679
            $opt      = "IS NOT NULL";
680
            $distinct = 'DISTINCT';
681
        }
682
        $params[] = $this->tree->getTreeId();
683
        $total =
684
            Database::prepare(
685
                "SELECT COUNT({$distinct} n_surn COLLATE '" . I18N::collation() . "')" .
686
                " FROM `##name`" .
687
                " WHERE n_surn COLLATE '" . I18N::collation() . "' {$opt} AND n_file=?"
688
            )->execute(
689
                $params
690
            )->fetchOne();
691
692
        return I18N::number($total);
0 ignored issues
show
Bug introduced by
It seems like $total can also be of type string; however, parameter $n of Fisharebest\Webtrees\I18N::number() does only seem to accept double, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

692
        return I18N::number(/** @scrutinizer ignore-type */ $total);
Loading history...
693
    }
694
695
    /**
696
     * Count the number of distinct given names, or count the number of
697
     * occurrences of a specific name or names.
698
     *
699
     * @param string[] $params
700
     *
701
     * @return string
702
     */
703
    public function totalGivennames($params = array())
704
    {
705
        if ($params) {
706
            $qs       = implode(',', array_fill(0, count($params), '?'));
707
            $params[] = $this->tree->getTreeId();
708
            $total    =
709
                Database::prepare("SELECT COUNT( n_givn) FROM `##name` WHERE n_givn IN ({$qs}) AND n_file=?")
710
                    ->execute($params)
711
                    ->fetchOne();
712
        } else {
713
            $total =
714
                Database::prepare("SELECT COUNT(DISTINCT n_givn) FROM `##name` WHERE n_givn IS NOT NULL AND n_file=?")
715
                    ->execute(array($this->tree->getTreeId()))
716
                    ->fetchOne();
717
        }
718
719
        return I18N::number($total);
0 ignored issues
show
Bug introduced by
It seems like $total can also be of type string; however, parameter $n of Fisharebest\Webtrees\I18N::number() does only seem to accept double, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

719
        return I18N::number(/** @scrutinizer ignore-type */ $total);
Loading history...
720
    }
721
722
    /**
723
     * Count the number of events (with dates).
724
     *
725
     * @param string[] $params
726
     *
727
     * @return string
728
     */
729
    public function totalEvents($params = array())
730
    {
731
        $sql  = "SELECT COUNT(*) AS tot FROM `##dates` WHERE d_file=?";
732
        $vars = array($this->tree->getTreeId());
733
734
        $no_types = array('HEAD', 'CHAN');
735
        if ($params) {
736
            $types = array();
737
            foreach ($params as $type) {
738
                if (substr($type, 0, 1) == '!') {
739
                    $no_types[] = substr($type, 1);
740
                } else {
741
                    $types[] = $type;
742
                }
743
            }
744
            if ($types) {
745
                $sql .= ' AND d_fact IN (' . implode(', ', array_fill(0, count($types), '?')) . ')';
746
                $vars = array_merge($vars, $types);
747
            }
748
        }
749
        $sql .= ' AND d_fact NOT IN (' . implode(', ', array_fill(0, count($no_types), '?')) . ')';
750
        $vars = array_merge($vars, $no_types);
751
752
        return I18N::number(Database::prepare($sql)->execute($vars)->fetchOne());
0 ignored issues
show
Bug introduced by
It seems like Fisharebest\Webtrees\Dat...cute($vars)->fetchOne() can also be of type string; however, parameter $n of Fisharebest\Webtrees\I18N::number() does only seem to accept double, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

752
        return I18N::number(/** @scrutinizer ignore-type */ Database::prepare($sql)->execute($vars)->fetchOne());
Loading history...
753
    }
754
755
    /**
756
     * Count the number of births.
757
     *
758
     * @return string
759
     */
760
    public function totalEventsBirth()
761
    {
762
        return $this->totalEvents(explode('|', WT_EVENTS_BIRT));
763
    }
764
765
    /**
766
     * Count the number of births.
767
     *
768
     * @return string
769
     */
770
    public function totalBirths()
771
    {
772
        return $this->totalEvents(array('BIRT'));
773
    }
774
775
    /**
776
     * Count the number of deaths.
777
     *
778
     * @return string
779
     */
780
    public function totalEventsDeath()
781
    {
782
        return $this->totalEvents(explode('|', WT_EVENTS_DEAT));
783
    }
784
785
    /**
786
     * Count the number of deaths.
787
     *
788
     * @return string
789
     */
790
    public function totalDeaths()
791
    {
792
        return $this->totalEvents(array('DEAT'));
793
    }
794
795
    /**
796
     * Count the number of marriages.
797
     *
798
     * @return string
799
     */
800
    public function totalEventsMarriage()
801
    {
802
        return $this->totalEvents(explode('|', WT_EVENTS_MARR));
803
    }
804
805
    /**
806
     * Count the number of marriages.
807
     *
808
     * @return string
809
     */
810
    public function totalMarriages()
811
    {
812
        return $this->totalEvents(array('MARR'));
813
    }
814
815
    /**
816
     * Count the number of divorces.
817
     *
818
     * @return string
819
     */
820
    public function totalEventsDivorce()
821
    {
822
        return $this->totalEvents(explode('|', WT_EVENTS_DIV));
823
    }
824
825
    /**
826
     * Count the number of divorces.
827
     *
828
     * @return string
829
     */
830
    public function totalDivorces()
831
    {
832
        return $this->totalEvents(array('DIV'));
833
    }
834
835
    /**
836
     * Count the number of other events.
837
     *
838
     * @return string
839
     */
840
    public function totalEventsOther()
841
    {
842
        $facts    = array_merge(explode('|', WT_EVENTS_BIRT . '|' . WT_EVENTS_MARR . '|' . WT_EVENTS_DIV . '|' . WT_EVENTS_DEAT));
843
        $no_facts = array();
844
        foreach ($facts as $fact) {
845
            $fact       = '!' . str_replace('\'', '', $fact);
846
            $no_facts[] = $fact;
847
        }
848
849
        return $this->totalEvents($no_facts);
850
    }
851
852
    /**
853
     * Count the number of males.
854
     *
855
     * @return int
856
     */
857
    private function totalSexMalesQuery()
858
    {
859
        return (int) Database::prepare(
860
            "SELECT COUNT(*) FROM `##individuals` WHERE i_file = :tree_id AND i_sex = 'M'"
861
        )->execute(array(
862
            'tree_id' => $this->tree->getTreeId(),
863
        ))->fetchOne();
864
    }
865
866
    /**
867
     * Count the number of males.
868
     *
869
     * @return string
870
     */
871
    public function totalSexMales()
872
    {
873
        return I18N::number($this->totalSexMalesQuery());
874
    }
875
876
    /**
877
     * Count the number of males
878
     *
879
     * @return string
880
     */
881
    public function totalSexMalesPercentage()
882
    {
883
        return $this->getPercentage($this->totalSexMalesQuery(), 'individual');
884
    }
885
886
    /**
887
     * Count the number of females.
888
     *
889
     * @return int
890
     */
891
    private function totalSexFemalesQuery()
892
    {
893
        return (int) Database::prepare(
894
            "SELECT COUNT(*) FROM `##individuals` WHERE i_file = :tree_id AND i_sex = 'F'"
895
        )->execute(array(
896
            'tree_id' => $this->tree->getTreeId(),
897
        ))->fetchOne();
898
    }
899
900
    /**
901
     * Count the number of females.
902
     *
903
     * @return string
904
     */
905
    public function totalSexFemales()
906
    {
907
        return I18N::number($this->totalSexFemalesQuery());
908
    }
909
910
    /**
911
     * Count the number of females.
912
     *
913
     * @return string
914
     */
915
    public function totalSexFemalesPercentage()
916
    {
917
        return $this->getPercentage($this->totalSexFemalesQuery(), 'individual');
918
    }
919
920
    /**
921
     * Count the number of individuals with unknown sex.
922
     *
923
     * @return int
924
     */
925
    private function totalSexUnknownQuery()
926
    {
927
        return (int) Database::prepare(
928
            "SELECT COUNT(*) FROM `##individuals` WHERE i_file = :tree_id AND i_sex = 'U'"
929
        )->execute(array(
930
            'tree_id' => $this->tree->getTreeId(),
931
        ))->fetchOne();
932
    }
933
934
    /**
935
     * Count the number of individuals with unknown sex.
936
     *
937
     * @return string
938
     */
939
    public function totalSexUnknown()
940
    {
941
        return I18N::number($this->totalSexUnknownQuery());
942
    }
943
944
    /**
945
     * Count the number of individuals with unknown sex.
946
     *
947
     * @return string
948
     */
949
    public function totalSexUnknownPercentage()
950
    {
951
        return $this->getPercentage($this->totalSexUnknownQuery(), 'individual');
952
    }
953
954
    /**
955
     * Generate a chart showing sex distribution.
956
     *
957
     * @param string[] $params
958
     *
959
     * @return string
960
     */
961
    public function chartSex($params = array())
962
    {
963
        $WT_STATS_S_CHART_X    = Theme::theme()->parameter('stats-small-chart-x');
964
        $WT_STATS_S_CHART_Y    = Theme::theme()->parameter('stats-small-chart-y');
965
966
        if (isset($params[0]) && $params[0] != '') {
967
            $size = strtolower($params[0]);
968
        } else {
969
            $size = $WT_STATS_S_CHART_X . "x" . $WT_STATS_S_CHART_Y;
970
        }
971
        if (isset($params[1]) && $params[1] != '') {
972
            $color_female = strtolower($params[1]);
973
        } else {
974
            $color_female = 'ffd1dc';
975
        }
976
        if (isset($params[2]) && $params[2] != '') {
977
            $color_male = strtolower($params[2]);
978
        } else {
979
            $color_male = '84beff';
980
        }
981
        if (isset($params[3]) && $params[3] != '') {
982
            $color_unknown = strtolower($params[3]);
983
        } else {
984
            $color_unknown = '777777';
985
        }
986
        $sizes = explode('x', $size);
987
        // Raw data - for calculation
988
        $tot_f = $this->totalSexFemalesQuery();
989
        $tot_m = $this->totalSexMalesQuery();
990
        $tot_u = $this->totalSexUnknownQuery();
991
        $tot   = $tot_f + $tot_m + $tot_u;
992
        // I18N data - for display
993
        $per_f = $this->totalSexFemalesPercentage();
994
        $per_m = $this->totalSexMalesPercentage();
995
        $per_u = $this->totalSexUnknownPercentage();
996
        if ($tot == 0) {
997
            return '';
998
        } elseif ($tot_u > 0) {
999
            $chd = $this->arrayToExtendedEncoding(array(4095 * $tot_u / $tot, 4095 * $tot_f / $tot, 4095 * $tot_m / $tot));
1000
            $chl =
1001
                I18N::translateContext('unknown people', 'Unknown') . ' - ' . $per_u . '|' .
1002
                I18N::translate('Females') . ' - ' . $per_f . '|' .
1003
                I18N::translate('Males') . ' - ' . $per_m;
1004
            $chart_title =
1005
                I18N::translate('Males') . ' - ' . $per_m . I18N::$list_separator .
1006
                I18N::translate('Females') . ' - ' . $per_f . I18N::$list_separator .
1007
                I18N::translateContext('unknown people', 'Unknown') . ' - ' . $per_u;
1008
1009
            return "<img src=\"https://chart.googleapis.com/chart?cht=p3&amp;chd=e:{$chd}&amp;chs={$size}&amp;chco={$color_unknown},{$color_female},{$color_male}&amp;chf=bg,s,ffffff00&amp;chl={$chl}\" width=\"{$sizes[0]}\" height=\"{$sizes[1]}\" alt=\"" . $chart_title . "\" title=\"" . $chart_title . "\" />";
1010
        } else {
1011
            $chd = $this->arrayToExtendedEncoding(array(4095 * $tot_f / $tot, 4095 * $tot_m / $tot));
1012
            $chl =
1013
                I18N::translate('Females') . ' - ' . $per_f . '|' .
1014
                I18N::translate('Males') . ' - ' . $per_m;
1015
            $chart_title = I18N::translate('Males') . ' - ' . $per_m . I18N::$list_separator .
1016
                I18N::translate('Females') . ' - ' . $per_f;
1017
1018
            return "<img src=\"https://chart.googleapis.com/chart?cht=p3&amp;chd=e:{$chd}&amp;chs={$size}&amp;chco={$color_female},{$color_male}&amp;chf=bg,s,ffffff00&amp;chl={$chl}\" width=\"{$sizes[0]}\" height=\"{$sizes[1]}\" alt=\"" . $chart_title . "\" title=\"" . $chart_title . "\" />";
1019
        }
1020
    }
1021
1022
    /**
1023
     * Count the number of living individuals.
1024
     *
1025
     * The totalLiving/totalDeceased queries assume that every dead person will
1026
     * have a DEAT record. It will not include individuals who were born more
1027
     * than MAX_ALIVE_AGE years ago, and who have no DEAT record.
1028
     * A good reason to run the “Add missing DEAT records” batch-update!
1029
     *
1030
     * @return int
1031
     */
1032
    private function totalLivingQuery()
1033
    {
1034
        return (int) Database::prepare(
1035
            "SELECT COUNT(*) FROM `##individuals` WHERE i_file = :tree_id AND i_gedcom NOT REGEXP '\\n1 (" . WT_EVENTS_DEAT . ")'"
1036
        )->execute(array(
1037
            'tree_id' => $this->tree->getTreeId(),
1038
        ))->fetchOne();
1039
    }
1040
1041
    /**
1042
     * Count the number of living individuals.
1043
     *
1044
     * @return string
1045
     */
1046
    public function totalLiving()
1047
    {
1048
        return I18N::number($this->totalLivingQuery());
1049
    }
1050
1051
    /**
1052
     * Count the number of living individuals.
1053
     *
1054
     * @return string
1055
     */
1056
    public function totalLivingPercentage()
1057
    {
1058
        return $this->getPercentage($this->totalLivingQuery(), 'individual');
1059
    }
1060
1061
    /**
1062
     * Count the number of dead individuals.
1063
     *
1064
     * @return int
1065
     */
1066
    private function totalDeceasedQuery()
1067
    {
1068
        return (int) Database::prepare(
1069
            "SELECT COUNT(*) FROM `##individuals` WHERE i_file = :tree_id AND i_gedcom REGEXP '\\n1 (" . WT_EVENTS_DEAT . ")'"
1070
        )->execute(array(
1071
            'tree_id' => $this->tree->getTreeId(),
1072
        ))->fetchOne();
1073
    }
1074
1075
    /**
1076
     * Count the number of dead individuals.
1077
     *
1078
     * @return string
1079
     */
1080
    public function totalDeceased()
1081
    {
1082
        return I18N::number($this->totalDeceasedQuery());
1083
    }
1084
1085
    /**
1086
     * Count the number of dead individuals.
1087
     *
1088
     * @return string
1089
     */
1090
    public function totalDeceasedPercentage()
1091
    {
1092
        return $this->getPercentage($this->totalDeceasedQuery(), 'individual');
1093
    }
1094
1095
    /**
1096
     * Create a chart showing mortality.
1097
     *
1098
     * @param string[] $params
1099
     *
1100
     * @return string
1101
     */
1102
    public function chartMortality($params = array())
1103
    {
1104
        $WT_STATS_S_CHART_X    = Theme::theme()->parameter('stats-small-chart-x');
1105
        $WT_STATS_S_CHART_Y    = Theme::theme()->parameter('stats-small-chart-y');
1106
1107
        if (isset($params[0]) && $params[0] != '') {
1108
            $size = strtolower($params[0]);
1109
        } else {
1110
            $size = $WT_STATS_S_CHART_X . "x" . $WT_STATS_S_CHART_Y;
1111
        }
1112
        if (isset($params[1]) && $params[1] != '') {
1113
            $color_living = strtolower($params[1]);
1114
        } else {
1115
            $color_living = 'ffffff';
1116
        }
1117
        if (isset($params[2]) && $params[2] != '') {
1118
            $color_dead = strtolower($params[2]);
1119
        } else {
1120
            $color_dead = 'cccccc';
1121
        }
1122
        $sizes = explode('x', $size);
1123
        // Raw data - for calculation
1124
        $tot_l = $this->totalLivingQuery();
1125
        $tot_d = $this->totalDeceasedQuery();
1126
        $tot   = $tot_l + $tot_d;
1127
        // I18N data - for display
1128
        $per_l = $this->totalLivingPercentage();
1129
        $per_d = $this->totalDeceasedPercentage();
1130
        if ($tot == 0) {
1131
            return '';
1132
        } else {
1133
            $chd = $this->arrayToExtendedEncoding(array(4095 * $tot_l / $tot, 4095 * $tot_d / $tot));
1134
            $chl =
1135
                I18N::translate('Living') . ' - ' . $per_l . '|' .
1136
                I18N::translate('Dead') . ' - ' . $per_d . '|';
1137
            $chart_title = I18N::translate('Living') . ' - ' . $per_l . I18N::$list_separator .
1138
                I18N::translate('Dead') . ' - ' . $per_d;
1139
1140
            return "<img src=\"https://chart.googleapis.com/chart?cht=p3&amp;chd=e:{$chd}&amp;chs={$size}&amp;chco={$color_living},{$color_dead}&amp;chf=bg,s,ffffff00&amp;chl={$chl}\" width=\"{$sizes[0]}\" height=\"{$sizes[1]}\" alt=\"" . $chart_title . "\" title=\"" . $chart_title . "\" />";
1141
        }
1142
    }
1143
1144
    /**
1145
     * Count the number of users.
1146
     *
1147
     * @param string[] $params
1148
     *
1149
     * @return string
1150
     */
1151
    public function totalUsers($params = array())
1152
    {
1153
        if (isset($params[0])) {
1154
            $total = count(User::all()) + (int) $params[0];
1155
        } else {
1156
            $total = count(User::all());
1157
        }
1158
1159
        return I18N::number($total);
1160
    }
1161
1162
    /**
1163
     * Count the number of administrators.
1164
     *
1165
     * @return string
1166
     */
1167
    public function totalAdmins()
1168
    {
1169
        return I18N::number(count(User::allAdmins()));
1170
    }
1171
1172
    /**
1173
     * Count the number of administrators.
1174
     *
1175
     * @return string
1176
     */
1177
    public function totalNonAdmins()
1178
    {
1179
        return I18N::number(count(User::all()) - count(User::allAdmins()));
1180
    }
1181
1182
    /**
1183
     * Count the number of media records with a given type.
1184
     *
1185
     * @param string $type
1186
     *
1187
     * @return int
1188
     */
1189
    private function totalMediaType($type = 'all')
1190
    {
1191
        if (!in_array($type, $this->_media_types) && $type != 'all' && $type != 'unknown') {
1192
            return 0;
1193
        }
1194
        $sql  = "SELECT COUNT(*) AS tot FROM `##media` WHERE m_file=?";
1195
        $vars = array($this->tree->getTreeId());
1196
1197
        if ($type != 'all') {
1198
            if ($type == 'unknown') {
1199
                // There has to be a better way then this :(
1200
                foreach ($this->_media_types as $t) {
1201
                    $sql .= " AND (m_gedcom NOT LIKE ? AND m_gedcom NOT LIKE ?)";
1202
                    $vars[] = "%3 TYPE {$t}%";
1203
                    $vars[] = "%1 _TYPE {$t}%";
1204
                }
1205
            } else {
1206
                $sql .= " AND (m_gedcom LIKE ? OR m_gedcom LIKE ?)";
1207
                $vars[] = "%3 TYPE {$type}%";
1208
                $vars[] = "%1 _TYPE {$type}%";
1209
            }
1210
        }
1211
1212
        return (int) Database::prepare($sql)->execute($vars)->fetchOne();
1213
    }
1214
1215
    /**
1216
     * Count the number of media records.
1217
     *
1218
     * @return string
1219
     */
1220
    public function totalMedia()
1221
    {
1222
        return I18N::number($this->totalMediaType('all'));
1223
    }
1224
1225
    /**
1226
     * Count the number of media records with type "audio".
1227
     *
1228
     * @return string
1229
     */
1230
    public function totalMediaAudio()
1231
    {
1232
        return I18N::number($this->totalMediaType('audio'));
1233
    }
1234
1235
    /**
1236
     * Count the number of media records with type "book".
1237
     *
1238
     * @return string
1239
     */
1240
    public function totalMediaBook()
1241
    {
1242
        return I18N::number($this->totalMediaType('book'));
1243
    }
1244
1245
    /**
1246
     * Count the number of media records with type "card".
1247
     *
1248
     * @return string
1249
     */
1250
    public function totalMediaCard()
1251
    {
1252
        return I18N::number($this->totalMediaType('card'));
1253
    }
1254
1255
    /**
1256
     * Count the number of media records with type "certificate".
1257
     *
1258
     * @return string
1259
     */
1260
    public function totalMediaCertificate()
1261
    {
1262
        return I18N::number($this->totalMediaType('certificate'));
1263
    }
1264
1265
    /**
1266
     * Count the number of media records with type "coat of arms".
1267
     *
1268
     * @return string
1269
     */
1270
    public function totalMediaCoatOfArms()
1271
    {
1272
        return I18N::number($this->totalMediaType('coat'));
1273
    }
1274
1275
    /**
1276
     * Count the number of media records with type "document".
1277
     *
1278
     * @return string
1279
     */
1280
    public function totalMediaDocument()
1281
    {
1282
        return I18N::number($this->totalMediaType('document'));
1283
    }
1284
1285
    /**
1286
     * Count the number of media records with type "electronic".
1287
     *
1288
     * @return string
1289
     */
1290
    public function totalMediaElectronic()
1291
    {
1292
        return I18N::number($this->totalMediaType('electronic'));
1293
    }
1294
1295
    /**
1296
     * Count the number of media records with type "magazine".
1297
     *
1298
     * @return string
1299
     */
1300
    public function totalMediaMagazine()
1301
    {
1302
        return I18N::number($this->totalMediaType('magazine'));
1303
    }
1304
1305
    /**
1306
     * Count the number of media records with type "manuscript".
1307
     *
1308
     * @return string
1309
     */
1310
    public function totalMediaManuscript()
1311
    {
1312
        return I18N::number($this->totalMediaType('manuscript'));
1313
    }
1314
1315
    /**
1316
     * Count the number of media records with type "map".
1317
     *
1318
     * @return string
1319
     */
1320
    public function totalMediaMap()
1321
    {
1322
        return I18N::number($this->totalMediaType('map'));
1323
    }
1324
1325
    /**
1326
     * Count the number of media records with type "microfiche".
1327
     *
1328
     * @return string
1329
     */
1330
    public function totalMediaFiche()
1331
    {
1332
        return I18N::number($this->totalMediaType('fiche'));
1333
    }
1334
1335
    /**
1336
     * Count the number of media records with type "microfilm".
1337
     *
1338
     * @return string
1339
     */
1340
    public function totalMediaFilm()
1341
    {
1342
        return I18N::number($this->totalMediaType('film'));
1343
    }
1344
1345
    /**
1346
     * Count the number of media records with type "newspaper".
1347
     *
1348
     * @return string
1349
     */
1350
    public function totalMediaNewspaper()
1351
    {
1352
        return I18N::number($this->totalMediaType('newspaper'));
1353
    }
1354
1355
    /**
1356
     * Count the number of media records with type "painting".
1357
     *
1358
     * @return string
1359
     */
1360
    public function totalMediaPainting()
1361
    {
1362
        return I18N::number($this->totalMediaType('painting'));
1363
    }
1364
1365
    /**
1366
     * Count the number of media records with type "photograph".
1367
     *
1368
     * @return string
1369
     */
1370
    public function totalMediaPhoto()
1371
    {
1372
        return I18N::number($this->totalMediaType('photo'));
1373
    }
1374
1375
    /**
1376
     * Count the number of media records with type "tombstone".
1377
     *
1378
     * @return string
1379
     */
1380
    public function totalMediaTombstone()
1381
    {
1382
        return I18N::number($this->totalMediaType('tombstone'));
1383
    }
1384
1385
    /**
1386
     * Count the number of media records with type "video".
1387
     *
1388
     * @return string
1389
     */
1390
    public function totalMediaVideo()
1391
    {
1392
        return I18N::number($this->totalMediaType('video'));
1393
    }
1394
1395
    /**
1396
     * Count the number of media records with type "other".
1397
     *
1398
     * @return string
1399
     */
1400
    public function totalMediaOther()
1401
    {
1402
        return I18N::number($this->totalMediaType('other'));
1403
    }
1404
1405
    /**
1406
     * Count the number of media records with type "unknown".
1407
     *
1408
     * @return string
1409
     */
1410
    public function totalMediaUnknown()
1411
    {
1412
        return I18N::number($this->totalMediaType('unknown'));
1413
    }
1414
1415
    /**
1416
     * Create a chart of media types.
1417
     *
1418
     * @param string[] $params
1419
     *
1420
     * @return string
1421
     */
1422
    public function chartMedia($params = array())
1423
    {
1424
        $WT_STATS_CHART_COLOR1 = Theme::theme()->parameter('distribution-chart-no-values');
1425
        $WT_STATS_CHART_COLOR2 = Theme::theme()->parameter('distribution-chart-high-values');
1426
        $WT_STATS_S_CHART_X    = Theme::theme()->parameter('stats-small-chart-x');
1427
        $WT_STATS_S_CHART_Y    = Theme::theme()->parameter('stats-small-chart-y');
1428
1429
        if (isset($params[0]) && $params[0] != '') {
1430
            $size = strtolower($params[0]);
1431
        } else {
1432
            $size = $WT_STATS_S_CHART_X . 'x' . $WT_STATS_S_CHART_Y;
1433
        }
1434
        if (isset($params[1]) && $params[1] != '') {
1435
            $color_from = strtolower($params[1]);
1436
        } else {
1437
            $color_from = $WT_STATS_CHART_COLOR1;
1438
        }
1439
        if (isset($params[2]) && $params[2] != '') {
1440
            $color_to = strtolower($params[2]);
1441
        } else {
1442
            $color_to = $WT_STATS_CHART_COLOR2;
1443
        }
1444
        $sizes = explode('x', $size);
1445
        $tot   = $this->totalMediaType('all');
1446
        // Beware divide by zero
1447
        if ($tot == 0) {
1448
            return I18N::translate('None');
1449
        }
1450
        // Build a table listing only the media types actually present in the GEDCOM
1451
        $mediaCounts = array();
1452
        $mediaTypes  = "";
1453
        $chart_title = "";
1454
        $c           = 0;
1455
        $max         = 0;
1456
        $media       = array();
1457
        foreach ($this->_media_types as $type) {
1458
            $count = $this->totalMediaType($type);
1459
            if ($count > 0) {
1460
                $media[$type] = $count;
1461
                if ($count > $max) {
1462
                    $max = $count;
1463
                }
1464
                $c += $count;
1465
            }
1466
        }
1467
        $count = $this->totalMediaType('unknown');
1468
        if ($count > 0) {
1469
            $media['unknown'] = $tot - $c;
1470
            if ($tot - $c > $max) {
1471
                $max = $count;
1472
            }
1473
        }
1474
        if (($max / $tot) > 0.6 && count($media) > 10) {
1475
            arsort($media);
1476
            $media = array_slice($media, 0, 10);
1477
            $c     = $tot;
1478
            foreach ($media as $cm) {
1479
                $c -= $cm;
1480
            }
1481
            if (isset($media['other'])) {
1482
                $media['other'] += $c;
1483
            } else {
1484
                $media['other'] = $c;
1485
            }
1486
        }
1487
        asort($media);
1488
        foreach ($media as $type => $count) {
1489
            $mediaCounts[] = round(100 * $count / $tot, 0);
1490
            $mediaTypes .= GedcomTag::getFileFormTypeValue($type) . ' - ' . I18N::number($count) . '|';
1491
            $chart_title .= GedcomTag::getFileFormTypeValue($type) . ' (' . $count . '), ';
1492
        }
1493
        $chart_title = substr($chart_title, 0, -2);
1494
        $chd         = $this->arrayToExtendedEncoding($mediaCounts);
0 ignored issues
show
Bug introduced by
It seems like $mediaCounts can also be of type array<mixed,double>; however, parameter $a of Fisharebest\Webtrees\Sta...rayToExtendedEncoding() does only seem to accept integer[], maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

1494
        $chd         = $this->arrayToExtendedEncoding(/** @scrutinizer ignore-type */ $mediaCounts);
Loading history...
1495
        $chl         = substr($mediaTypes, 0, -1);
1496
1497
        return "<img src=\"https://chart.googleapis.com/chart?cht=p3&amp;chd=e:{$chd}&amp;chs={$size}&amp;chco={$color_from},{$color_to}&amp;chf=bg,s,ffffff00&amp;chl={$chl}\" width=\"{$sizes[0]}\" height=\"{$sizes[1]}\" alt=\"" . $chart_title . "\" title=\"" . $chart_title . "\" />";
1498
    }
1499
1500
    /**
1501
     * Birth and Death
1502
     *
1503
     * @param string $type
1504
     * @param string $life_dir
1505
     * @param string $birth_death
1506
     *
1507
     * @return string
1508
     */
1509
    private function mortalityQuery($type = 'full', $life_dir = 'ASC', $birth_death = 'BIRT')
1510
    {
1511
        if ($birth_death == 'MARR') {
1512
            $query_field = "'MARR'";
1513
        } elseif ($birth_death == 'DIV') {
1514
            $query_field = "'DIV'";
1515
        } elseif ($birth_death == 'BIRT') {
1516
            $query_field = "'BIRT'";
1517
        } else {
1518
            $query_field = "'DEAT'";
1519
        }
1520
        if ($life_dir == 'ASC') {
1521
            $dmod = 'MIN';
1522
        } else {
1523
            $dmod = 'MAX';
1524
        }
1525
        $rows = $this->runSql(
1526
            "SELECT d_year, d_type, d_fact, d_gid" .
1527
            " FROM `##dates`" .
1528
            " WHERE d_file={$this->tree->getTreeId()} AND d_fact IN ({$query_field}) AND d_julianday1=(" .
1529
            " SELECT {$dmod}( d_julianday1 )" .
1530
            " FROM `##dates`" .
1531
            " WHERE d_file={$this->tree->getTreeId()} AND d_fact IN ({$query_field}) AND d_julianday1<>0 )" .
1532
            " LIMIT 1"
1533
        );
1534
        if (!isset($rows[0])) {
1535
            return '';
1536
        }
1537
        $row    = $rows[0];
1538
        $record = GedcomRecord::getInstance($row['d_gid'], $this->tree);
1539
        switch ($type) {
1540
            default:
1541
            case 'full':
1542
                if ($record->canShow()) {
1543
                    $result = $record->formatList('span', false, $record->getFullName());
1544
                } else {
1545
                    $result = I18N::translate('This information is private and cannot be shown.');
1546
                }
1547
                break;
1548
            case 'year':
1549
                $date   = new Date($row['d_type'] . ' ' . $row['d_year']);
1550
                $result = $date->display();
1551
                break;
1552
            case 'name':
1553
                $result = "<a href=\"" . $record->getHtmlUrl() . "\">" . $record->getFullName() . "</a>";
1554
                break;
1555
            case 'place':
1556
                $fact = GedcomRecord::getInstance($row['d_gid'], $this->tree)->getFirstFact($row['d_fact']);
1557
                if ($fact) {
1558
                    $result = FunctionsPrint::formatFactPlace($fact, true, true, true);
1559
                } else {
1560
                    $result = I18N::translate('Private');
1561
                }
1562
                break;
1563
        }
1564
1565
        return $result;
1566
    }
1567
1568
    /**
1569
     * Places
1570
     *
1571
     * @param string $what
1572
     * @param string $fact
1573
     * @param int    $parent
1574
     * @param bool   $country
1575
     *
1576
     * @return int[]|string[][]
1577
     */
1578
    public function statsPlaces($what = 'ALL', $fact = '', $parent = 0, $country = false)
1579
    {
1580
        if ($fact) {
1581
            if ($what == 'INDI') {
1582
                $rows = Database::prepare(
1583
                    "SELECT i_gedcom AS ged FROM `##individuals` WHERE i_file = :tree_id AND i_gedcom LIKE '%\n2 PLAC %'"
1584
                )->execute(array(
1585
                    'tree_id' => $this->tree->getTreeId(),
1586
                ))->fetchAll();
1587
            } elseif ($what == 'FAM') {
1588
                $rows = Database::prepare(
1589
                    "SELECT f_gedcom AS ged FROM `##families` WHERE f_file = :tree_id AND f_gedcom LIKE '%\n2 PLAC %'"
1590
                )->execute(array(
1591
                    'tree_id' => $this->tree->getTreeId(),
1592
                ))->fetchAll();
1593
            }
1594
            $placelist = array();
1595
            foreach ($rows as $row) {
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $rows does not seem to be defined for all execution paths leading up to this point.
Loading history...
1596
                if (preg_match('/\n1 ' . $fact . '(?:\n[2-9].*)*\n2 PLAC (.+)/', $row->ged, $match)) {
1597
                    if ($country) {
1598
                        $tmp   = explode(Place::GEDCOM_SEPARATOR, $match[1]);
1599
                        $place = end($tmp);
1600
                    } else {
1601
                        $place = $match[1];
1602
                    }
1603
                    if (!isset($placelist[$place])) {
1604
                        $placelist[$place] = 1;
1605
                    } else {
1606
                        $placelist[$place]++;
1607
                    }
1608
                }
1609
            }
1610
1611
            return $placelist;
1612
        } elseif ($parent > 0) {
1613
            // used by placehierarchy googlemap module
1614
            if ($what == 'INDI') {
1615
                $join = " JOIN `##individuals` ON pl_file = i_file AND pl_gid = i_id";
1616
            } elseif ($what == 'FAM') {
1617
                $join = " JOIN `##families` ON pl_file = f_file AND pl_gid = f_id";
1618
            } else {
1619
                $join = "";
1620
            }
1621
            $rows = $this->runSql(
1622
                " SELECT" .
1623
                " p_place AS place," .
1624
                " COUNT(*) AS tot" .
1625
                " FROM" .
1626
                " `##places`" .
1627
                " JOIN `##placelinks` ON pl_file=p_file AND p_id=pl_p_id" .
1628
                $join .
1629
                " WHERE" .
1630
                " p_id={$parent} AND" .
1631
                " p_file={$this->tree->getTreeId()}" .
1632
                " GROUP BY place"
1633
            );
1634
1635
            return $rows;
1636
        } else {
1637
            if ($what == 'INDI') {
1638
                $join = " JOIN `##individuals` ON pl_file = i_file AND pl_gid = i_id";
1639
            } elseif ($what == 'FAM') {
1640
                $join = " JOIN `##families` ON pl_file = f_file AND pl_gid = f_id";
1641
            } else {
1642
                $join = "";
1643
            }
1644
            $rows = $this->runSql(
1645
                " SELECT" .
1646
                " p_place AS country," .
1647
                " COUNT(*) AS tot" .
1648
                " FROM" .
1649
                " `##places`" .
1650
                " JOIN `##placelinks` ON pl_file=p_file AND p_id=pl_p_id" .
1651
                $join .
1652
                " WHERE" .
1653
                " p_file={$this->tree->getTreeId()}" .
1654
                " AND p_parent_id='0'" .
1655
                " GROUP BY country ORDER BY tot DESC, country ASC"
1656
            );
1657
1658
            return $rows;
1659
        }
1660
    }
1661
1662
    /**
1663
     * Count total places.
1664
     *
1665
     * @return int
1666
     */
1667
    private function totalPlacesQuery()
1668
    {
1669
        return
1670
            (int) Database::prepare("SELECT COUNT(*) FROM `##places` WHERE p_file=?")
1671
                ->execute(array($this->tree->getTreeId()))
1672
                ->fetchOne();
1673
    }
1674
1675
    /**
1676
     * Count total places.
1677
     *
1678
     * @return string
1679
     */
1680
    public function totalPlaces()
1681
    {
1682
        return I18N::number($this->totalPlacesQuery());
1683
    }
1684
1685
    /**
1686
     * Create a chart showing where events occurred.
1687
     *
1688
     * @param string[] $params
1689
     *
1690
     * @return string
1691
     */
1692
    public function chartDistribution($params = array())
1693
    {
1694
        $WT_STATS_CHART_COLOR1 = Theme::theme()->parameter('distribution-chart-no-values');
1695
        $WT_STATS_CHART_COLOR2 = Theme::theme()->parameter('distribution-chart-high-values');
1696
        $WT_STATS_CHART_COLOR3 = Theme::theme()->parameter('distribution-chart-low-values');
1697
        $WT_STATS_MAP_X        = Theme::theme()->parameter('distribution-chart-x');
1698
        $WT_STATS_MAP_Y        = Theme::theme()->parameter('distribution-chart-y');
1699
1700
        if (isset($params[0])) {
1701
            $chart_shows = $params[0];
1702
        } else {
1703
            $chart_shows = 'world';
1704
        }
1705
        if (isset($params[1])) {
1706
            $chart_type = $params[1];
1707
        } else {
1708
            $chart_type = '';
1709
        }
1710
        if (isset($params[2])) {
1711
            $surname = $params[2];
1712
        } else {
1713
            $surname = '';
1714
        }
1715
1716
        if ($this->totalPlacesQuery() == 0) {
1717
            return '';
1718
        }
1719
        // Get the country names for each language
1720
        $country_to_iso3166 = array();
1721
        foreach (I18N::activeLocales() as $locale) {
1722
            I18N::init($locale->languageTag());
1723
            $countries = $this->getAllCountries();
1724
            foreach ($this->iso3166() as $three => $two) {
1725
                $country_to_iso3166[$three]             = $two;
1726
                $country_to_iso3166[$countries[$three]] = $two;
1727
            }
1728
        }
1729
        I18N::init(WT_LOCALE);
1730
        switch ($chart_type) {
1731
            case 'surname_distribution_chart':
1732
                if ($surname == "") {
1733
                    $surname = $this->getCommonSurname();
1734
                }
1735
                $chart_title = I18N::translate('Surname distribution chart') . ': ' . $surname;
1736
                // Count how many people are events in each country
1737
                $surn_countries = array();
1738
                $indis          = QueryName::individuals($this->tree, I18N::strtoupper($surname), '', '', false, false);
1739
                foreach ($indis as $person) {
1740
                    if (preg_match_all('/^2 PLAC (?:.*, *)*(.*)/m', $person->getGedcom(), $matches)) {
1741
                        // webtrees uses 3 letter country codes and localised country names, but google uses 2 letter codes.
1742
                        foreach ($matches[1] as $country) {
1743
                            if (array_key_exists($country, $country_to_iso3166)) {
1744
                                if (array_key_exists($country_to_iso3166[$country], $surn_countries)) {
1745
                                    $surn_countries[$country_to_iso3166[$country]]++;
1746
                                } else {
1747
                                    $surn_countries[$country_to_iso3166[$country]] = 1;
1748
                                }
1749
                            }
1750
                        }
1751
                    }
1752
                };
1753
                break;
1754
            case 'birth_distribution_chart':
1755
                $chart_title = I18N::translate('Birth by country');
1756
                // Count how many people were born in each country
1757
                $surn_countries = array();
1758
                $b_countries    = $this->statsPlaces('INDI', 'BIRT', 0, true);
1759
                foreach ($b_countries as $place => $count) {
1760
                    $country = $place;
1761
                    if (array_key_exists($country, $country_to_iso3166)) {
1762
                        if (!isset($surn_countries[$country_to_iso3166[$country]])) {
1763
                            $surn_countries[$country_to_iso3166[$country]] = $count;
1764
                        } else {
1765
                            $surn_countries[$country_to_iso3166[$country]] += $count;
1766
                        }
1767
                    }
1768
                }
1769
                break;
1770
            case 'death_distribution_chart':
1771
                $chart_title = I18N::translate('Death by country');
1772
                // Count how many people were death in each country
1773
                $surn_countries = array();
1774
                $d_countries    = $this->statsPlaces('INDI', 'DEAT', 0, true);
1775
                foreach ($d_countries as $place => $count) {
1776
                    $country = $place;
1777
                    if (array_key_exists($country, $country_to_iso3166)) {
1778
                        if (!isset($surn_countries[$country_to_iso3166[$country]])) {
1779
                            $surn_countries[$country_to_iso3166[$country]] = $count;
1780
                        } else {
1781
                            $surn_countries[$country_to_iso3166[$country]] += $count;
1782
                        }
1783
                    }
1784
                }
1785
                break;
1786
            case 'marriage_distribution_chart':
1787
                $chart_title = I18N::translate('Marriage by country');
1788
                // Count how many families got marriage in each country
1789
                $surn_countries = array();
1790
                $m_countries    = $this->statsPlaces('FAM');
1791
                // webtrees uses 3 letter country codes and localised country names, but google uses 2 letter codes.
1792
                foreach ($m_countries as $place) {
1793
                    $country = $place['country'];
1794
                    if (array_key_exists($country, $country_to_iso3166)) {
1795
                        if (!isset($surn_countries[$country_to_iso3166[$country]])) {
1796
                            $surn_countries[$country_to_iso3166[$country]] = $place['tot'];
1797
                        } else {
1798
                            $surn_countries[$country_to_iso3166[$country]] += $place['tot'];
1799
                        }
1800
                    }
1801
                }
1802
                break;
1803
            case 'indi_distribution_chart':
1804
            default:
1805
                $chart_title = I18N::translate('Individual distribution chart');
1806
                // Count how many people have events in each country
1807
                $surn_countries = array();
1808
                $a_countries    = $this->statsPlaces('INDI');
1809
                // webtrees uses 3 letter country codes and localised country names, but google uses 2 letter codes.
1810
                foreach ($a_countries as $place) {
1811
                    $country = $place['country'];
1812
                    if (array_key_exists($country, $country_to_iso3166)) {
1813
                        if (!isset($surn_countries[$country_to_iso3166[$country]])) {
1814
                            $surn_countries[$country_to_iso3166[$country]] = $place['tot'];
1815
                        } else {
1816
                            $surn_countries[$country_to_iso3166[$country]] += $place['tot'];
1817
                        }
1818
                    }
1819
                }
1820
                break;
1821
        }
1822
        $chart_url = "https://chart.googleapis.com/chart?cht=t&amp;chtm=" . $chart_shows;
1823
        $chart_url .= "&amp;chco=" . $WT_STATS_CHART_COLOR1 . "," . $WT_STATS_CHART_COLOR3 . "," . $WT_STATS_CHART_COLOR2; // country colours
1824
        $chart_url .= "&amp;chf=bg,s,ECF5FF"; // sea colour
1825
        $chart_url .= "&amp;chs=" . $WT_STATS_MAP_X . "x" . $WT_STATS_MAP_Y;
1826
        $chart_url .= "&amp;chld=" . implode('', array_keys($surn_countries)) . "&amp;chd=s:";
1827
        foreach ($surn_countries as $count) {
1828
            $chart_url .= substr(WT_GOOGLE_CHART_ENCODING, (int) ($count / max($surn_countries) * 61), 1);
1829
        }
1830
        $chart = '<div id="google_charts" class="center">';
1831
        $chart .= '<p>' . $chart_title . '</p>';
1832
        $chart .= '<div><img src="' . $chart_url . '" alt="' . $chart_title . '" title="' . $chart_title . '" class="gchart" /><br>';
1833
        $chart .= '<table class="center"><tr>';
1834
        $chart .= '<td bgcolor="#' . $WT_STATS_CHART_COLOR2 . '" width="12"></td><td>' . I18N::translate('Highest population') . '</td>';
1835
        $chart .= '<td bgcolor="#' . $WT_STATS_CHART_COLOR3 . '" width="12"></td><td>' . I18N::translate('Lowest population') . '</td>';
1836
        $chart .= '<td bgcolor="#' . $WT_STATS_CHART_COLOR1 . '" width="12"></td><td>' . I18N::translate('Nobody at all') . '</td>';
1837
        $chart .= '</tr></table></div></div>';
1838
1839
        return $chart;
1840
    }
1841
1842
    /**
1843
     * A list of common countries.
1844
     *
1845
     * @return string
1846
     */
1847
    public function commonCountriesList()
1848
    {
1849
        $countries = $this->statsPlaces();
1850
        if (empty($countries)) {
1851
            return '';
1852
        }
1853
        $top10 = array();
1854
        $i     = 1;
1855
        // Get the country names for each language
1856
        $country_names = array();
1857
        foreach (I18N::activeLocales() as $locale) {
1858
            I18N::init($locale->languageTag());
1859
            $all_countries = $this->getAllCountries();
1860
            foreach ($all_countries as $country_code => $country_name) {
1861
                $country_names[$country_name] = $country_code;
1862
            }
1863
        }
1864
        I18N::init(WT_LOCALE);
1865
        $all_db_countries = array();
1866
        foreach ($countries as $place) {
1867
            $country = trim($place['country']);
1868
            if (array_key_exists($country, $country_names)) {
1869
                if (!isset($all_db_countries[$country_names[$country]][$country])) {
1870
                    $all_db_countries[$country_names[$country]][$country] = (int) $place['tot'];
1871
                } else {
1872
                    $all_db_countries[$country_names[$country]][$country] += (int) $place['tot'];
1873
                }
1874
            }
1875
        }
1876
        // get all the user’s countries names
1877
        $all_countries = $this->getAllCountries();
1878
        foreach ($all_db_countries as $country_code => $country) {
1879
            $top10[] = '<li>';
1880
            foreach ($country as $country_name => $tot) {
1881
                $tmp   = new Place($country_name, $this->tree);
1882
                $place = '<a href="' . $tmp->getURL() . '" class="list_item">' . $all_countries[$country_code] . '</a>';
1883
                $top10[] .= $place . ' - ' . I18N::number($tot);
1884
            }
1885
            $top10[] .= '</li>';
1886
            if ($i++ == 10) {
1887
                break;
1888
            }
1889
        }
1890
        $top10 = implode('', $top10);
1891
1892
        return '<ul>' . $top10 . '</ul>';
1893
    }
1894
1895
    /**
1896
     * A list of common birth places.
1897
     *
1898
     * @return string
1899
     */
1900
    public function commonBirthPlacesList()
1901
    {
1902
        $places = $this->statsPlaces('INDI', 'BIRT');
1903
        $top10  = array();
1904
        $i      = 1;
1905
        arsort($places);
1906
        foreach ($places as $place => $count) {
1907
            $tmp     = new Place($place, $this->tree);
1908
            $place   = '<a href="' . $tmp->getURL() . '" class="list_item">' . $tmp->getFullName() . '</a>';
1909
            $top10[] = '<li>' . $place . ' - ' . I18N::number($count) . '</li>';
1910
            if ($i++ == 10) {
1911
                break;
1912
            }
1913
        }
1914
        $top10 = implode('', $top10);
1915
1916
        return '<ul>' . $top10 . '</ul>';
1917
    }
1918
1919
    /**
1920
     * A list of common death places.
1921
     *
1922
     * @return string
1923
     */
1924
    public function commonDeathPlacesList()
1925
    {
1926
        $places = $this->statsPlaces('INDI', 'DEAT');
1927
        $top10  = array();
1928
        $i      = 1;
1929
        arsort($places);
1930
        foreach ($places as $place => $count) {
1931
            $tmp     = new Place($place, $this->tree);
1932
            $place   = '<a href="' . $tmp->getURL() . '" class="list_item">' . $tmp->getFullName() . '</a>';
1933
            $top10[] = '<li>' . $place . ' - ' . I18N::number($count) . '</li>';
1934
            if ($i++ == 10) {
1935
                break;
1936
            }
1937
        }
1938
        $top10 = implode('', $top10);
1939
1940
        return '<ul>' . $top10 . '</ul>';
1941
    }
1942
1943
    /**
1944
     * A list of common marriage places.
1945
     *
1946
     * @return string
1947
     */
1948
    public function commonMarriagePlacesList()
1949
    {
1950
        $places = $this->statsPlaces('FAM', 'MARR');
1951
        $top10  = array();
1952
        $i      = 1;
1953
        arsort($places);
1954
        foreach ($places as $place => $count) {
1955
            $tmp     = new Place($place, $this->tree);
1956
            $place   = '<a href="' . $tmp->getURL() . '" class="list_item">' . $tmp->getFullName() . '</a>';
1957
            $top10[] = '<li>' . $place . ' - ' . I18N::number($count) . '</li>';
1958
            if ($i++ == 10) {
1959
                break;
1960
            }
1961
        }
1962
        $top10 = implode('', $top10);
1963
1964
        return '<ul>' . $top10 . '</ul>';
1965
    }
1966
1967
    /**
1968
     * Create a chart of birth places.
1969
     *
1970
     * @param bool     $simple
1971
     * @param bool     $sex
1972
     * @param int      $year1
1973
     * @param int      $year2
1974
     * @param string[] $params
1975
     *
1976
     * @return array|string
1977
     */
1978
    public function statsBirthQuery($simple = true, $sex = false, $year1 = -1, $year2 = -1, $params = array())
1979
    {
1980
        $WT_STATS_CHART_COLOR1 = Theme::theme()->parameter('distribution-chart-no-values');
1981
        $WT_STATS_CHART_COLOR2 = Theme::theme()->parameter('distribution-chart-high-values');
1982
        $WT_STATS_S_CHART_X    = Theme::theme()->parameter('stats-small-chart-x');
1983
        $WT_STATS_S_CHART_Y    = Theme::theme()->parameter('stats-small-chart-y');
1984
1985
        if ($simple) {
1986
            $sql =
1987
                "SELECT FLOOR(d_year/100+1) AS century, COUNT(*) AS total FROM `##dates` " .
1988
                "WHERE " .
1989
                "d_file={$this->tree->getTreeId()} AND " .
1990
                "d_year<>0 AND " .
1991
                "d_fact='BIRT' AND " .
1992
                "d_type IN ('@#DGREGORIAN@', '@#DJULIAN@')";
1993
        } elseif ($sex) {
1994
            $sql =
1995
                "SELECT d_month, i_sex, COUNT(*) AS total FROM `##dates` " .
1996
                "JOIN `##individuals` ON d_file = i_file AND d_gid = i_id " .
1997
                "WHERE " .
1998
                "d_file={$this->tree->getTreeId()} AND " .
1999
                "d_fact='BIRT' AND " .
2000
                "d_type IN ('@#DGREGORIAN@', '@#DJULIAN@')";
2001
        } else {
2002
            $sql =
2003
                "SELECT d_month, COUNT(*) AS total FROM `##dates` " .
2004
                "WHERE " .
2005
                "d_file={$this->tree->getTreeId()} AND " .
2006
                "d_fact='BIRT' AND " .
2007
                "d_type IN ('@#DGREGORIAN@', '@#DJULIAN@')";
2008
        }
2009
        if ($year1 >= 0 && $year2 >= 0) {
2010
            $sql .= " AND d_year BETWEEN '{$year1}' AND '{$year2}'";
2011
        }
2012
        if ($simple) {
2013
            $sql .= " GROUP BY century ORDER BY century";
2014
        } else {
2015
            $sql .= " GROUP BY d_month";
2016
            if ($sex) {
2017
                $sql .= ", i_sex";
2018
            }
2019
        }
2020
        $rows = $this->runSql($sql);
2021
        if ($simple) {
2022
            if (isset($params[0]) && $params[0] != '') {
2023
                $size = strtolower($params[0]);
2024
            } else {
2025
                $size = $WT_STATS_S_CHART_X . "x" . $WT_STATS_S_CHART_Y;
2026
            }
2027
            if (isset($params[1]) && $params[1] != '') {
2028
                $color_from = strtolower($params[1]);
2029
            } else {
2030
                $color_from = $WT_STATS_CHART_COLOR1;
2031
            }
2032
            if (isset($params[2]) && $params[2] != '') {
2033
                $color_to = strtolower($params[2]);
2034
            } else {
2035
                $color_to = $WT_STATS_CHART_COLOR2;
2036
            }
2037
            $sizes = explode('x', $size);
2038
            $tot   = 0;
2039
            foreach ($rows as $values) {
2040
                $tot += $values['total'];
2041
            }
2042
            // Beware divide by zero
2043
            if ($tot == 0) {
0 ignored issues
show
introduced by
The condition $tot == 0 is always true.
Loading history...
2044
                return '';
2045
            }
2046
            $centuries = "";
2047
            $counts    = array();
2048
            foreach ($rows as $values) {
2049
                $counts[] = round(100 * $values['total'] / $tot, 0);
2050
                $centuries .= $this->centuryName($values['century']) . ' - ' . I18N::number($values['total']) . '|';
2051
            }
2052
            $chd = $this->arrayToExtendedEncoding($counts);
2053
            $chl = rawurlencode(substr($centuries, 0, -1));
2054
2055
            return "<img src=\"https://chart.googleapis.com/chart?cht=p3&amp;chd=e:{$chd}&amp;chs={$size}&amp;chco={$color_from},{$color_to}&amp;chf=bg,s,ffffff00&amp;chl={$chl}\" width=\"{$sizes[0]}\" height=\"{$sizes[1]}\" alt=\"" . I18N::translate('Births by century') . "\" title=\"" . I18N::translate('Births by century') . "\" />";
2056
        } else {
2057
            return $rows;
2058
        }
2059
    }
2060
2061
    /**
2062
     * Create a chart of death places.
2063
     *
2064
     * @param bool     $simple
2065
     * @param bool     $sex
2066
     * @param int      $year1
2067
     * @param int      $year2
2068
     * @param string[] $params
2069
     *
2070
     * @return array|string
2071
     */
2072
    public function statsDeathQuery($simple = true, $sex = false, $year1 = -1, $year2 = -1, $params = array())
2073
    {
2074
        $WT_STATS_CHART_COLOR1 = Theme::theme()->parameter('distribution-chart-no-values');
2075
        $WT_STATS_CHART_COLOR2 = Theme::theme()->parameter('distribution-chart-high-values');
2076
        $WT_STATS_S_CHART_X    = Theme::theme()->parameter('stats-small-chart-x');
2077
        $WT_STATS_S_CHART_Y    = Theme::theme()->parameter('stats-small-chart-y');
2078
2079
        if ($simple) {
2080
            $sql =
2081
                "SELECT FLOOR(d_year/100+1) AS century, COUNT(*) AS total FROM `##dates` " .
2082
                "WHERE " .
2083
                "d_file={$this->tree->getTreeId()} AND " .
2084
                'd_year<>0 AND ' .
2085
                "d_fact='DEAT' AND " .
2086
                "d_type IN ('@#DGREGORIAN@', '@#DJULIAN@')";
2087
        } elseif ($sex) {
2088
            $sql =
2089
                "SELECT d_month, i_sex, COUNT(*) AS total FROM `##dates` " .
2090
                "JOIN `##individuals` ON d_file = i_file AND d_gid = i_id " .
2091
                "WHERE " .
2092
                "d_file={$this->tree->getTreeId()} AND " .
2093
                "d_fact='DEAT' AND " .
2094
                "d_type IN ('@#DGREGORIAN@', '@#DJULIAN@')";
2095
        } else {
2096
            $sql =
2097
                "SELECT d_month, COUNT(*) AS total FROM `##dates` " .
2098
                "WHERE " .
2099
                "d_file={$this->tree->getTreeId()} AND " .
2100
                "d_fact='DEAT' AND " .
2101
                "d_type IN ('@#DGREGORIAN@', '@#DJULIAN@')";
2102
        }
2103
        if ($year1 >= 0 && $year2 >= 0) {
2104
            $sql .= " AND d_year BETWEEN '{$year1}' AND '{$year2}'";
2105
        }
2106
        if ($simple) {
2107
            $sql .= " GROUP BY century ORDER BY century";
2108
        } else {
2109
            $sql .= " GROUP BY d_month";
2110
            if ($sex) {
2111
                $sql .= ", i_sex";
2112
            }
2113
        }
2114
        $rows = $this->runSql($sql);
2115
        if ($simple) {
2116
            if (isset($params[0]) && $params[0] != '') {
2117
                $size = strtolower($params[0]);
2118
            } else {
2119
                $size = $WT_STATS_S_CHART_X . "x" . $WT_STATS_S_CHART_Y;
2120
            }
2121
            if (isset($params[1]) && $params[1] != '') {
2122
                $color_from = strtolower($params[1]);
2123
            } else {
2124
                $color_from = $WT_STATS_CHART_COLOR1;
2125
            }
2126
            if (isset($params[2]) && $params[2] != '') {
2127
                $color_to = strtolower($params[2]);
2128
            } else {
2129
                $color_to = $WT_STATS_CHART_COLOR2;
2130
            }
2131
            $sizes = explode('x', $size);
2132
            $tot   = 0;
2133
            foreach ($rows as $values) {
2134
                $tot += $values['total'];
2135
            }
2136
            // Beware divide by zero
2137
            if ($tot == 0) {
0 ignored issues
show
introduced by
The condition $tot == 0 is always true.
Loading history...
2138
                return '';
2139
            }
2140
            $centuries = "";
2141
            $counts    = array();
2142
            foreach ($rows as $values) {
2143
                $counts[] = round(100 * $values['total'] / $tot, 0);
2144
                $centuries .= $this->centuryName($values['century']) . ' - ' . I18N::number($values['total']) . '|';
2145
            }
2146
            $chd = $this->arrayToExtendedEncoding($counts);
2147
            $chl = rawurlencode(substr($centuries, 0, -1));
2148
2149
            return "<img src=\"https://chart.googleapis.com/chart?cht=p3&amp;chd=e:{$chd}&amp;chs={$size}&amp;chco={$color_from},{$color_to}&amp;chf=bg,s,ffffff00&amp;chl={$chl}\" width=\"{$sizes[0]}\" height=\"{$sizes[1]}\" alt=\"" . I18N::translate('Deaths by century') . "\" title=\"" . I18N::translate('Deaths by century') . "\" />";
2150
        }
2151
2152
        return $rows;
2153
    }
2154
2155
    /**
2156
     * Find the earliest birth.
2157
     *
2158
     * @return string
2159
     */
2160
    public function firstBirth()
2161
    {
2162
        return $this->mortalityQuery('full', 'ASC', 'BIRT');
2163
    }
2164
2165
    /**
2166
     * Find the earliest birth year.
2167
     *
2168
     * @return string
2169
     */
2170
    public function firstBirthYear()
2171
    {
2172
        return $this->mortalityQuery('year', 'ASC', 'BIRT');
2173
    }
2174
2175
    /**
2176
     * Find the name of the earliest birth.
2177
     *
2178
     * @return string
2179
     */
2180
    public function firstBirthName()
2181
    {
2182
        return $this->mortalityQuery('name', 'ASC', 'BIRT');
2183
    }
2184
2185
    /**
2186
     * Find the earliest birth place.
2187
     *
2188
     * @return string
2189
     */
2190
    public function firstBirthPlace()
2191
    {
2192
        return $this->mortalityQuery('place', 'ASC', 'BIRT');
2193
    }
2194
2195
    /**
2196
     * Find the latest birth.
2197
     *
2198
     * @return string
2199
     */
2200
    public function lastBirth()
2201
    {
2202
        return $this->mortalityQuery('full', 'DESC', 'BIRT');
2203
    }
2204
2205
    /**
2206
     * Find the latest birth year.
2207
     *
2208
     * @return string
2209
     */
2210
    public function lastBirthYear()
2211
    {
2212
        return $this->mortalityQuery('year', 'DESC', 'BIRT');
2213
    }
2214
2215
    /**
2216
     * Find the latest birth name.
2217
     *
2218
     * @return string
2219
     */
2220
    public function lastBirthName()
2221
    {
2222
        return $this->mortalityQuery('name', 'DESC', 'BIRT');
2223
    }
2224
2225
    /**
2226
     * Find the latest birth place.
2227
     *
2228
     * @return string
2229
     */
2230
    public function lastBirthPlace()
2231
    {
2232
        return $this->mortalityQuery('place', 'DESC', 'BIRT');
2233
    }
2234
2235
    /**
2236
     * General query on births.
2237
     *
2238
     * @param string[] $params
2239
     *
2240
     * @return string
2241
     */
2242
    public function statsBirth($params = array())
2243
    {
2244
        return $this->statsBirthQuery(true, false, -1, -1, $params);
2245
    }
2246
2247
    /**
2248
     * Find the earliest death.
2249
     *
2250
     * @return string
2251
     */
2252
    public function firstDeath()
2253
    {
2254
        return $this->mortalityQuery('full', 'ASC', 'DEAT');
2255
    }
2256
2257
    /**
2258
     * Find the earliest death year.
2259
     *
2260
     * @return string
2261
     */
2262
    public function firstDeathYear()
2263
    {
2264
        return $this->mortalityQuery('year', 'ASC', 'DEAT');
2265
    }
2266
2267
    /**
2268
     * Find the earliest death name.
2269
     *
2270
     * @return string
2271
     */
2272
    public function firstDeathName()
2273
    {
2274
        return $this->mortalityQuery('name', 'ASC', 'DEAT');
2275
    }
2276
2277
    /**
2278
     * Find the earliest death place.
2279
     *
2280
     * @return string
2281
     */
2282
    public function firstDeathPlace()
2283
    {
2284
        return $this->mortalityQuery('place', 'ASC', 'DEAT');
2285
    }
2286
2287
    /**
2288
     * Find the latest death.
2289
     *
2290
     * @return string
2291
     */
2292
    public function lastDeath()
2293
    {
2294
        return $this->mortalityQuery('full', 'DESC', 'DEAT');
2295
    }
2296
2297
    /**
2298
     * Find the latest death year.
2299
     *
2300
     * @return string
2301
     */
2302
    public function lastDeathYear()
2303
    {
2304
        return $this->mortalityQuery('year', 'DESC', 'DEAT');
2305
    }
2306
2307
    /**
2308
     * Find the latest death name.
2309
     *
2310
     * @return string
2311
     */
2312
    public function lastDeathName()
2313
    {
2314
        return $this->mortalityQuery('name', 'DESC', 'DEAT');
2315
    }
2316
2317
    /**
2318
     * Find the place of the latest death.
2319
     *
2320
     * @return string
2321
     */
2322
    public function lastDeathPlace()
2323
    {
2324
        return $this->mortalityQuery('place', 'DESC', 'DEAT');
2325
    }
2326
2327
    /**
2328
     * General query on deaths.
2329
     *
2330
     * @param string[] $params
2331
     *
2332
     * @return string
2333
     */
2334
    public function statsDeath($params = array())
2335
    {
2336
        return $this->statsDeathQuery(true, false, -1, -1, $params);
2337
    }
2338
2339
    /**
2340
     * Lifespan
2341
     *
2342
     * @param string $type
2343
     * @param string $sex
2344
     *
2345
     * @return string
2346
     */
2347
    private function longlifeQuery($type = 'full', $sex = 'F')
2348
    {
2349
        $sex_search = ' 1=1';
2350
        if ($sex == 'F') {
2351
            $sex_search = " i_sex='F'";
2352
        } elseif ($sex == 'M') {
2353
            $sex_search = " i_sex='M'";
2354
        }
2355
2356
        $rows = $this->runSql(
2357
            " SELECT" .
2358
            " death.d_gid AS id," .
2359
            " death.d_julianday2-birth.d_julianday1 AS age" .
2360
            " FROM" .
2361
            " `##dates` AS death," .
2362
            " `##dates` AS birth," .
2363
            " `##individuals` AS indi" .
2364
            " WHERE" .
2365
            " indi.i_id=birth.d_gid AND" .
2366
            " birth.d_gid=death.d_gid AND" .
2367
            " death.d_file={$this->tree->getTreeId()} AND" .
2368
            " birth.d_file=death.d_file AND" .
2369
            " birth.d_file=indi.i_file AND" .
2370
            " birth.d_fact='BIRT' AND" .
2371
            " death.d_fact='DEAT' AND" .
2372
            " birth.d_julianday1<>0 AND" .
2373
            " death.d_julianday1>birth.d_julianday2 AND" .
2374
            $sex_search .
2375
            " ORDER BY" .
2376
            " age DESC LIMIT 1"
2377
        );
2378
        if (!isset($rows[0])) {
2379
            return '';
2380
        }
2381
        $row    = $rows[0];
2382
        $person = Individual::getInstance($row['id'], $this->tree);
2383
        switch ($type) {
2384
            default:
2385
            case 'full':
2386
                if ($person->canShowName()) {
2387
                    $result = $person->formatList('span', false, $person->getFullName());
2388
                } else {
2389
                    $result = I18N::translate('This information is private and cannot be shown.');
2390
                }
2391
                break;
2392
            case 'age':
2393
                $result = I18N::number((int) ($row['age'] / 365.25));
2394
                break;
2395
            case 'name':
2396
                $result = "<a href=\"" . $person->getHtmlUrl() . "\">" . $person->getFullName() . "</a>";
2397
                break;
2398
        }
2399
2400
        return $result;
2401
    }
2402
2403
    /**
2404
     * Find the oldest individuals.
2405
     *
2406
     * @param string   $type
2407
     * @param string   $sex
2408
     * @param string[] $params
2409
     *
2410
     * @return string
2411
     */
2412
    private function topTenOldestQuery($type = 'list', $sex = 'BOTH', $params = array())
2413
    {
2414
        if ($sex === 'F') {
2415
            $sex_search = " AND i_sex='F' ";
2416
        } elseif ($sex === 'M') {
2417
            $sex_search = " AND i_sex='M' ";
2418
        } else {
2419
            $sex_search = '';
2420
        }
2421
        if (isset($params[0])) {
2422
            $total = (int) $params[0];
2423
        } else {
2424
            $total = 10;
2425
        }
2426
        $rows = $this->runSql(
2427
            "SELECT " .
2428
            " MAX(death.d_julianday2-birth.d_julianday1) AS age, " .
2429
            " death.d_gid AS deathdate " .
2430
            "FROM " .
2431
            " `##dates` AS death, " .
2432
            " `##dates` AS birth, " .
2433
            " `##individuals` AS indi " .
2434
            "WHERE " .
2435
            " indi.i_id=birth.d_gid AND " .
2436
            " birth.d_gid=death.d_gid AND " .
2437
            " death.d_file={$this->tree->getTreeId()} AND " .
2438
            " birth.d_file=death.d_file AND " .
2439
            " birth.d_file=indi.i_file AND " .
2440
            " birth.d_fact='BIRT' AND " .
2441
            " death.d_fact='DEAT' AND " .
2442
            " birth.d_julianday1<>0 AND " .
2443
            " death.d_julianday1>birth.d_julianday2 " .
2444
            $sex_search .
2445
            "GROUP BY deathdate " .
2446
            "ORDER BY age DESC " .
2447
            "LIMIT " . $total
2448
        );
2449
        if (!isset($rows[0])) {
2450
            return '';
2451
        }
2452
        $top10 = array();
2453
        foreach ($rows as $row) {
2454
            $person = Individual::getInstance($row['deathdate'], $this->tree);
2455
            $age    = $row['age'];
2456
            if ((int) ($age / 365.25) > 0) {
2457
                $age = (int) ($age / 365.25) . 'y';
2458
            } elseif ((int) ($age / 30.4375) > 0) {
2459
                $age = (int) ($age / 30.4375) . 'm';
2460
            } else {
2461
                $age = $age . 'd';
2462
            }
2463
            $age = FunctionsDate::getAgeAtEvent($age);
2464
            if ($person->canShow()) {
2465
                if ($type == 'list') {
2466
                    $top10[] = "<li><a href=\"" . $person->getHtmlUrl() . "\">" . $person->getFullName() . "</a> (" . $age . ")" . "</li>";
2467
                } else {
2468
                    $top10[] = "<a href=\"" . $person->getHtmlUrl() . "\">" . $person->getFullName() . "</a> (" . $age . ")";
2469
                }
2470
            }
2471
        }
2472
        if ($type == 'list') {
2473
            $top10 = implode('', $top10);
2474
        } else {
2475
            $top10 = implode(' ', $top10);
2476
        }
2477
        if (I18N::direction() === 'rtl') {
2478
            $top10 = str_replace(array('[', ']', '(', ')', '+'), array('&rlm;[', '&rlm;]', '&rlm;(', '&rlm;)', '&rlm;+'), $top10);
2479
        }
2480
        if ($type == 'list') {
2481
            return '<ul>' . $top10 . '</ul>';
2482
        }
2483
2484
        return $top10;
2485
    }
2486
2487
    /**
2488
     * Find the oldest living individuals.
2489
     *
2490
     * @param string   $type
2491
     * @param string   $sex
2492
     * @param string[] $params
2493
     *
2494
     * @return string
2495
     */
2496
    private function topTenOldestAliveQuery($type = 'list', $sex = 'BOTH', $params = array())
2497
    {
2498
        if (!Auth::isMember($this->tree)) {
2499
            return I18N::translate('This information is private and cannot be shown.');
2500
        }
2501
        if ($sex == 'F') {
2502
            $sex_search = " AND i_sex='F'";
2503
        } elseif ($sex == 'M') {
2504
            $sex_search = " AND i_sex='M'";
2505
        } else {
2506
            $sex_search = '';
2507
        }
2508
        if (isset($params[0])) {
2509
            $total = (int) $params[0];
2510
        } else {
2511
            $total = 10;
2512
        }
2513
        $rows = $this->runSql(
2514
            "SELECT" .
2515
            " birth.d_gid AS id," .
2516
            " MIN(birth.d_julianday1) AS age" .
2517
            " FROM" .
2518
            " `##dates` AS birth," .
2519
            " `##individuals` AS indi" .
2520
            " WHERE" .
2521
            " indi.i_id=birth.d_gid AND" .
2522
            " indi.i_gedcom NOT REGEXP '\\n1 (" . WT_EVENTS_DEAT . ")' AND" .
2523
            " birth.d_file={$this->tree->getTreeId()} AND" .
2524
            " birth.d_fact='BIRT' AND" .
2525
            " birth.d_file=indi.i_file AND" .
2526
            " birth.d_julianday1<>0" .
2527
            $sex_search .
2528
            " GROUP BY id" .
2529
            " ORDER BY age" .
2530
            " ASC LIMIT " . $total
2531
        );
2532
        $top10 = array();
2533
        foreach ($rows as $row) {
2534
            $person = Individual::getInstance($row['id'], $this->tree);
2535
            $age    = (WT_CLIENT_JD - $row['age']);
2536
            if ((int) ($age / 365.25) > 0) {
2537
                $age = (int) ($age / 365.25) . 'y';
2538
            } elseif ((int) ($age / 30.4375) > 0) {
2539
                $age = (int) ($age / 30.4375) . 'm';
2540
            } else {
2541
                $age = $age . 'd';
2542
            }
2543
            $age = FunctionsDate::getAgeAtEvent($age);
2544
            if ($type === 'list') {
2545
                $top10[] = "<li><a href=\"" . $person->getHtmlUrl() . "\">" . $person->getFullName() . "</a> (" . $age . ")" . "</li>";
2546
            } else {
2547
                $top10[] = "<a href=\"" . $person->getHtmlUrl() . "\">" . $person->getFullName() . "</a> (" . $age . ")";
2548
            }
2549
        }
2550
        if ($type === 'list') {
2551
            $top10 = implode('', $top10);
2552
        } else {
2553
            $top10 = implode('; ', $top10);
2554
        }
2555
        if (I18N::direction() === 'rtl') {
2556
            $top10 = str_replace(array('[', ']', '(', ')', '+'), array('&rlm;[', '&rlm;]', '&rlm;(', '&rlm;)', '&rlm;+'), $top10);
2557
        }
2558
        if ($type === 'list') {
2559
            return '<ul>' . $top10 . '</ul>';
2560
        }
2561
2562
        return $top10;
2563
    }
2564
2565
    /**
2566
     * Find the average lifespan.
2567
     *
2568
     * @param string $sex
2569
     * @param bool   $show_years
2570
     *
2571
     * @return string
2572
     */
2573
    private function averageLifespanQuery($sex = 'BOTH', $show_years = false)
2574
    {
2575
        if ($sex === 'F') {
2576
            $sex_search = " AND i_sex='F' ";
2577
        } elseif ($sex === 'M') {
2578
            $sex_search = " AND i_sex='M' ";
2579
        } else {
2580
            $sex_search = '';
2581
        }
2582
        $rows = $this->runSql(
2583
            "SELECT " .
2584
            " AVG(death.d_julianday2-birth.d_julianday1) AS age " .
2585
            "FROM " .
2586
            " `##dates` AS death, " .
2587
            " `##dates` AS birth, " .
2588
            " `##individuals` AS indi " .
2589
            "WHERE " .
2590
            " indi.i_id=birth.d_gid AND " .
2591
            " birth.d_gid=death.d_gid AND " .
2592
            " death.d_file=" . $this->tree->getTreeId() . " AND " .
2593
            " birth.d_file=death.d_file AND " .
2594
            " birth.d_file=indi.i_file AND " .
2595
            " birth.d_fact='BIRT' AND " .
2596
            " death.d_fact='DEAT' AND " .
2597
            " birth.d_julianday1<>0 AND " .
2598
            " death.d_julianday1>birth.d_julianday2 " .
2599
            $sex_search
2600
        );
2601
        if (!isset($rows[0])) {
2602
            return '';
2603
        }
2604
        $row = $rows[0];
2605
        $age = $row['age'];
2606
        if ($show_years) {
2607
            if ((int) ($age / 365.25) > 0) {
2608
                $age = (int) ($age / 365.25) . 'y';
2609
            } elseif ((int) ($age / 30.4375) > 0) {
2610
                $age = (int) ($age / 30.4375) . 'm';
2611
            } elseif (!empty($age)) {
2612
                $age = $age . 'd';
2613
            }
2614
2615
            return FunctionsDate::getAgeAtEvent($age);
2616
        } else {
2617
            return I18N::number($age / 365.25);
2618
        }
2619
    }
2620
2621
    /**
2622
     * General query on ages.
2623
     *
2624
     * @param bool     $simple
2625
     * @param string   $related
2626
     * @param string   $sex
2627
     * @param int      $year1
2628
     * @param int      $year2
2629
     * @param string[] $params
2630
     *
2631
     * @return array|string
2632
     */
2633
    public function statsAgeQuery($simple = true, $related = 'BIRT', $sex = 'BOTH', $year1 = -1, $year2 = -1, $params = array())
2634
    {
2635
        if ($simple) {
2636
            if (isset($params[0]) && $params[0] != '') {
2637
                $size = strtolower($params[0]);
2638
            } else {
2639
                $size = '230x250';
2640
            }
2641
            $sizes = explode('x', $size);
2642
            $rows  = $this->runSql(
2643
                "SELECT" .
2644
                " ROUND(AVG(death.d_julianday2-birth.d_julianday1)/365.25,1) AS age," .
2645
                " FLOOR(death.d_year/100+1) AS century," .
2646
                " i_sex AS sex" .
2647
                " FROM" .
2648
                " `##dates` AS death," .
2649
                " `##dates` AS birth," .
2650
                " `##individuals` AS indi" .
2651
                " WHERE" .
2652
                " indi.i_id=birth.d_gid AND" .
2653
                " birth.d_gid=death.d_gid AND" .
2654
                " death.d_file={$this->tree->getTreeId()} AND" .
2655
                " birth.d_file=death.d_file AND" .
2656
                " birth.d_file=indi.i_file AND" .
2657
                " birth.d_fact='BIRT' AND" .
2658
                " death.d_fact='DEAT' AND" .
2659
                " birth.d_julianday1<>0 AND" .
2660
                " birth.d_type IN ('@#DGREGORIAN@', '@#DJULIAN@') AND" .
2661
                " death.d_type IN ('@#DGREGORIAN@', '@#DJULIAN@') AND" .
2662
                " death.d_julianday1>birth.d_julianday2" .
2663
                " GROUP BY century, sex ORDER BY century, sex");
2664
            if (empty($rows)) {
2665
                return '';
2666
            }
2667
            $chxl    = '0:|';
2668
            $countsm = '';
2669
            $countsf = '';
2670
            $countsa = '';
2671
            $out     = array();
2672
            foreach ($rows as $values) {
2673
                $out[$values['century']][$values['sex']] = $values['age'];
2674
            }
2675
            foreach ($out as $century => $values) {
2676
                if ($sizes[0] < 980) {
2677
                    $sizes[0] += 50;
2678
                }
2679
                $chxl .= $this->centuryName($century) . '|';
2680
                $average = 0;
2681
                if (isset($values['F'])) {
2682
                    $countsf .= $values['F'] . ',';
2683
                    $average = $values['F'];
2684
                } else {
2685
                    $countsf .= '0,';
2686
                }
2687
                if (isset($values['M'])) {
2688
                    $countsm .= $values['M'] . ',';
2689
                    if ($average == 0) {
2690
                        $countsa .= $values['M'] . ',';
2691
                    } else {
2692
                        $countsa .= (($values['M'] + $average) / 2) . ',';
2693
                    }
2694
                } else {
2695
                    $countsm .= '0,';
2696
                    if ($average == 0) {
2697
                        $countsa .= '0,';
2698
                    } else {
2699
                        $countsa .= $values['F'] . ',';
2700
                    }
2701
                }
2702
            }
2703
            $countsm = substr($countsm, 0, -1);
2704
            $countsf = substr($countsf, 0, -1);
2705
            $countsa = substr($countsa, 0, -1);
2706
            $chd     = 't2:' . $countsm . '|' . $countsf . '|' . $countsa;
2707
            $decades = '';
2708
            for ($i = 0; $i <= 100; $i += 10) {
2709
                $decades .= '|' . I18N::number($i);
2710
            }
2711
            $chxl .= '1:||' . I18N::translate('century') . '|2:' . $decades . '|3:||' . I18N::translate('Age') . '|';
2712
            $title = I18N::translate('Average age related to death century');
2713
            if (count($rows) > 6 || mb_strlen($title) < 30) {
2714
                $chtt = $title;
2715
            } else {
2716
                $offset  = 0;
2717
                $counter = array();
2718
                while ($offset = strpos($title, ' ', $offset + 1)) {
2719
                    $counter[] = $offset;
2720
                }
2721
                $half = (int) (count($counter) / 2);
2722
                $chtt = substr_replace($title, '|', $counter[$half], 1);
2723
            }
2724
2725
            return '<img src="' . "https://chart.googleapis.com/chart?cht=bvg&amp;chs={$sizes[0]}x{$sizes[1]}&amp;chm=D,FF0000,2,0,3,1|N*f1*,000000,0,-1,11,1|N*f1*,000000,1,-1,11,1&amp;chf=bg,s,ffffff00|c,s,ffffff00&amp;chtt=" . rawurlencode($chtt) . "&amp;chd={$chd}&amp;chco=0000FF,FFA0CB,FF0000&amp;chbh=20,3&amp;chxt=x,x,y,y&amp;chxl=" . rawurlencode($chxl) . "&amp;chdl=" . rawurlencode(I18N::translate('Males') . '|' . I18N::translate('Females') . '|' . I18N::translate('Average age at death')) . "\" width=\"{$sizes[0]}\" height=\"{$sizes[1]}\" alt=\"" . I18N::translate('Average age related to death century') . "\" title=\"" . I18N::translate('Average age related to death century') . "\" />";
0 ignored issues
show
Bug introduced by
It seems like $chtt can also be of type array; however, parameter $string of rawurlencode() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

2725
            return '<img src="' . "https://chart.googleapis.com/chart?cht=bvg&amp;chs={$sizes[0]}x{$sizes[1]}&amp;chm=D,FF0000,2,0,3,1|N*f1*,000000,0,-1,11,1|N*f1*,000000,1,-1,11,1&amp;chf=bg,s,ffffff00|c,s,ffffff00&amp;chtt=" . rawurlencode(/** @scrutinizer ignore-type */ $chtt) . "&amp;chd={$chd}&amp;chco=0000FF,FFA0CB,FF0000&amp;chbh=20,3&amp;chxt=x,x,y,y&amp;chxl=" . rawurlencode($chxl) . "&amp;chdl=" . rawurlencode(I18N::translate('Males') . '|' . I18N::translate('Females') . '|' . I18N::translate('Average age at death')) . "\" width=\"{$sizes[0]}\" height=\"{$sizes[1]}\" alt=\"" . I18N::translate('Average age related to death century') . "\" title=\"" . I18N::translate('Average age related to death century') . "\" />";
Loading history...
2726
        } else {
2727
            $sex_search = '';
2728
            $years      = '';
2729
            if ($sex == 'F') {
2730
                $sex_search = " AND i_sex='F'";
2731
            } elseif ($sex == 'M') {
2732
                $sex_search = " AND i_sex='M'";
2733
            }
2734
            if ($year1 >= 0 && $year2 >= 0) {
2735
                if ($related == 'BIRT') {
2736
                    $years = " AND birth.d_year BETWEEN '{$year1}' AND '{$year2}'";
2737
                } elseif ($related == 'DEAT') {
2738
                    $years = " AND death.d_year BETWEEN '{$year1}' AND '{$year2}'";
2739
                }
2740
            }
2741
            $rows = $this->runSql(
2742
                "SELECT" .
2743
                " death.d_julianday2-birth.d_julianday1 AS age" .
2744
                " FROM" .
2745
                " `##dates` AS death," .
2746
                " `##dates` AS birth," .
2747
                " `##individuals` AS indi" .
2748
                " WHERE" .
2749
                " indi.i_id=birth.d_gid AND" .
2750
                " birth.d_gid=death.d_gid AND" .
2751
                " death.d_file={$this->tree->getTreeId()} AND" .
2752
                " birth.d_file=death.d_file AND" .
2753
                " birth.d_file=indi.i_file AND" .
2754
                " birth.d_fact='BIRT' AND" .
2755
                " death.d_fact='DEAT' AND" .
2756
                " birth.d_julianday1<>0 AND" .
2757
                " birth.d_type IN ('@#DGREGORIAN@', '@#DJULIAN@') AND" .
2758
                " death.d_type IN ('@#DGREGORIAN@', '@#DJULIAN@') AND" .
2759
                " death.d_julianday1>birth.d_julianday2" .
2760
                $years .
2761
                $sex_search .
2762
                " ORDER BY age DESC");
2763
2764
            return $rows;
2765
        }
2766
    }
2767
2768
    /**
2769
     * General query on ages.
2770
     *
2771
     * @param string[] $params
2772
     *
2773
     * @return string
2774
     */
2775
    public function statsAge($params = array())
2776
    {
2777
        return $this->statsAgeQuery(true, 'BIRT', 'BOTH', -1, -1, $params);
2778
    }
2779
2780
    /**
2781
     * Find the lognest lived individual.
2782
     *
2783
     * @return string
2784
     */
2785
    public function longestLife()
2786
    {
2787
        return $this->longlifeQuery('full', 'BOTH');
2788
    }
2789
2790
    /**
2791
     * Find the age of the longest lived individual.
2792
     *
2793
     * @return string
2794
     */
2795
    public function longestLifeAge()
2796
    {
2797
        return $this->longlifeQuery('age', 'BOTH');
2798
    }
2799
2800
    /**
2801
     * Find the name of the longest lived individual.
2802
     *
2803
     * @return string
2804
     */
2805
    public function longestLifeName()
2806
    {
2807
        return $this->longlifeQuery('name', 'BOTH');
2808
    }
2809
2810
    /**
2811
     * Find the oldest individuals.
2812
     *
2813
     * @param string[] $params
2814
     *
2815
     * @return string
2816
     */
2817
    public function topTenOldest($params = array())
2818
    {
2819
        return $this->topTenOldestQuery('nolist', 'BOTH', $params);
2820
    }
2821
2822
    /**
2823
     * Find the oldest living individuals.
2824
     *
2825
     * @param string[] $params
2826
     *
2827
     * @return string
2828
     */
2829
    public function topTenOldestList($params = array())
2830
    {
2831
        return $this->topTenOldestQuery('list', 'BOTH', $params);
2832
    }
2833
2834
    /**
2835
     * Find the oldest living individuals.
2836
     *
2837
     * @param string[] $params
2838
     *
2839
     * @return string
2840
     */
2841
    public function topTenOldestAlive($params = array())
2842
    {
2843
        return $this->topTenOldestAliveQuery('nolist', 'BOTH', $params);
2844
    }
2845
2846
    /**
2847
     * Find the oldest living individuals.
2848
     *
2849
     * @param string[] $params
2850
     *
2851
     * @return string
2852
     */
2853
    public function topTenOldestListAlive($params = array())
2854
    {
2855
        return $this->topTenOldestAliveQuery('list', 'BOTH', $params);
2856
    }
2857
2858
    /**
2859
     * Find the average lifespan.
2860
     *
2861
     * @param bool $show_years
2862
     *
2863
     * @return string
2864
     */
2865
    public function averageLifespan($show_years = false)
2866
    {
2867
        return $this->averageLifespanQuery('BOTH', $show_years);
2868
    }
2869
2870
    /**
2871
     * Find the longest lived female.
2872
     *
2873
     * @return string
2874
     */
2875
    public function longestLifeFemale()
2876
    {
2877
        return $this->longlifeQuery('full', 'F');
2878
    }
2879
2880
    /**
2881
     * Find the age of the longest lived female.
2882
     *
2883
     * @return string
2884
     */
2885
    public function longestLifeFemaleAge()
2886
    {
2887
        return $this->longlifeQuery('age', 'F');
2888
    }
2889
2890
    /**
2891
     * Find the name of the longest lived female.
2892
     *
2893
     * @return string
2894
     */
2895
    public function longestLifeFemaleName()
2896
    {
2897
        return $this->longlifeQuery('name', 'F');
2898
    }
2899
2900
    /**
2901
     * Find the oldest females.
2902
     *
2903
     * @param string[] $params
2904
     *
2905
     * @return string
2906
     */
2907
    public function topTenOldestFemale($params = array())
2908
    {
2909
        return $this->topTenOldestQuery('nolist', 'F', $params);
2910
    }
2911
2912
    /**
2913
     * Find the oldest living females.
2914
     *
2915
     * @param string[] $params
2916
     *
2917
     * @return string
2918
     */
2919
    public function topTenOldestFemaleList($params = array())
2920
    {
2921
        return $this->topTenOldestQuery('list', 'F', $params);
2922
    }
2923
2924
    /**
2925
     * Find the oldest living females.
2926
     *
2927
     * @param string[] $params
2928
     *
2929
     * @return string
2930
     */
2931
    public function topTenOldestFemaleAlive($params = array())
2932
    {
2933
        return $this->topTenOldestAliveQuery('nolist', 'F', $params);
2934
    }
2935
2936
    /**
2937
     * Find the oldest living females.
2938
     *
2939
     * @param string[] $params
2940
     *
2941
     * @return string
2942
     */
2943
    public function topTenOldestFemaleListAlive($params = array())
2944
    {
2945
        return $this->topTenOldestAliveQuery('list', 'F', $params);
2946
    }
2947
2948
    /**
2949
     * Find the average lifespan of females.
2950
     *
2951
     * @param bool $show_years
2952
     *
2953
     * @return string
2954
     */
2955
    public function averageLifespanFemale($show_years = false)
2956
    {
2957
        return $this->averageLifespanQuery('F', $show_years);
2958
    }
2959
2960
    /**
2961
     * Find the longest lived male.
2962
     *
2963
     * @return string
2964
     */
2965
    public function longestLifeMale()
2966
    {
2967
        return $this->longlifeQuery('full', 'M');
2968
    }
2969
2970
    /**
2971
     * Find the age of the longest lived male.
2972
     *
2973
     * @return string
2974
     */
2975
    public function longestLifeMaleAge()
2976
    {
2977
        return $this->longlifeQuery('age', 'M');
2978
    }
2979
2980
    /**
2981
     * Find the name of the longest lived male.
2982
     *
2983
     * @return string
2984
     */
2985
    public function longestLifeMaleName()
2986
    {
2987
        return $this->longlifeQuery('name', 'M');
2988
    }
2989
2990
    /**
2991
     * Find the longest lived males.
2992
     *
2993
     * @param string[] $params
2994
     *
2995
     * @return string
2996
     */
2997
    public function topTenOldestMale($params = array())
2998
    {
2999
        return $this->topTenOldestQuery('nolist', 'M', $params);
3000
    }
3001
3002
    /**
3003
     * Find the longest lived males.
3004
     *
3005
     * @param string[] $params
3006
     *
3007
     * @return string
3008
     */
3009
    public function topTenOldestMaleList($params = array())
3010
    {
3011
        return $this->topTenOldestQuery('list', 'M', $params);
3012
    }
3013
3014
    /**
3015
     * Find the longest lived living males.
3016
     *
3017
     * @param string[] $params
3018
     *
3019
     * @return string
3020
     */
3021
    public function topTenOldestMaleAlive($params = array())
3022
    {
3023
        return $this->topTenOldestAliveQuery('nolist', 'M', $params);
3024
    }
3025
3026
    /**
3027
     * Find the longest lived living males.
3028
     *
3029
     * @param string[] $params
3030
     *
3031
     * @return string
3032
     */
3033
    public function topTenOldestMaleListAlive($params = array())
3034
    {
3035
        return $this->topTenOldestAliveQuery('list', 'M', $params);
3036
    }
3037
3038
    /**
3039
     * Find the average male lifespan.
3040
     *
3041
     * @param bool $show_years
3042
     *
3043
     * @return string
3044
     */
3045
    public function averageLifespanMale($show_years = false)
3046
    {
3047
        return $this->averageLifespanQuery('M', $show_years);
3048
    }
3049
3050
    /**
3051
     * Events
3052
     *
3053
     * @param string $type
3054
     * @param string $direction
3055
     * @param string $facts
3056
     *
3057
     * @return string
3058
     */
3059
    private function eventQuery($type, $direction, $facts)
3060
    {
3061
        $eventTypes = array(
3062
            'BIRT' => I18N::translate('birth'),
3063
            'DEAT' => I18N::translate('death'),
3064
            'MARR' => I18N::translate('marriage'),
3065
            'ADOP' => I18N::translate('adoption'),
3066
            'BURI' => I18N::translate('burial'),
3067
            'CENS' => I18N::translate('census added'),
3068
        );
3069
3070
        $fact_query = "IN ('" . str_replace('|', "','", $facts) . "')";
3071
3072
        if ($direction != 'ASC') {
3073
            $direction = 'DESC';
3074
        }
3075
        $rows = $this->runSql(''
3076
            . ' SELECT'
3077
            . ' d_gid AS id,'
3078
            . ' d_year AS year,'
3079
            . ' d_fact AS fact,'
3080
            . ' d_type AS type'
3081
            . ' FROM'
3082
            . " `##dates`"
3083
            . ' WHERE'
3084
            . " d_file={$this->tree->getTreeId()} AND"
3085
            . " d_gid<>'HEAD' AND"
3086
            . " d_fact {$fact_query} AND"
3087
            . ' d_julianday1<>0'
3088
            . ' ORDER BY'
3089
            . " d_julianday1 {$direction}, d_type LIMIT 1"
3090
        );
3091
        if (!isset($rows[0])) {
3092
            return '';
3093
        }
3094
        $row    = $rows[0];
3095
        $record = GedcomRecord::getInstance($row['id'], $this->tree);
3096
        switch ($type) {
3097
            default:
3098
            case 'full':
3099
                if ($record->canShow()) {
3100
                    $result = $record->formatList('span', false, $record->getFullName());
3101
                } else {
3102
                    $result = I18N::translate('This information is private and cannot be shown.');
3103
                }
3104
                break;
3105
            case 'year':
3106
                $date   = new Date($row['type'] . ' ' . $row['year']);
3107
                $result = $date->display();
3108
                break;
3109
            case 'type':
3110
                if (isset($eventTypes[$row['fact']])) {
3111
                    $result = $eventTypes[$row['fact']];
3112
                } else {
3113
                    $result = GedcomTag::getLabel($row['fact']);
3114
                }
3115
                break;
3116
            case 'name':
3117
                $result = "<a href=\"" . $record->getHtmlUrl() . "\">" . $record->getFullName() . "</a>";
3118
                break;
3119
            case 'place':
3120
                $fact = $record->getFirstFact($row['fact']);
3121
                if ($fact) {
3122
                    $result = FunctionsPrint::formatFactPlace($fact, true, true, true);
3123
                } else {
3124
                    $result = I18N::translate('Private');
3125
                }
3126
                break;
3127
        }
3128
3129
        return $result;
3130
    }
3131
3132
    /**
3133
     * Find the earliest event.
3134
     *
3135
     * @return string
3136
     */
3137
    public function firstEvent()
3138
    {
3139
        return $this->eventQuery('full', 'ASC', WT_EVENTS_BIRT . '|' . WT_EVENTS_MARR . '|' . WT_EVENTS_DIV . '|' . WT_EVENTS_DEAT);
3140
    }
3141
3142
    /**
3143
     * Find the year of the earliest event.
3144
     *
3145
     * @return string
3146
     */
3147
    public function firstEventYear()
3148
    {
3149
        return $this->eventQuery('year', 'ASC', WT_EVENTS_BIRT . '|' . WT_EVENTS_MARR . '|' . WT_EVENTS_DIV . '|' . WT_EVENTS_DEAT);
3150
    }
3151
3152
    /**
3153
     * Find the type of the earliest event.
3154
     *
3155
     * @return string
3156
     */
3157
    public function firstEventType()
3158
    {
3159
        return $this->eventQuery('type', 'ASC', WT_EVENTS_BIRT . '|' . WT_EVENTS_MARR . '|' . WT_EVENTS_DIV . '|' . WT_EVENTS_DEAT);
3160
    }
3161
3162
    /**
3163
     * Find the name of the individual with the earliest event.
3164
     *
3165
     * @return string
3166
     */
3167
    public function firstEventName()
3168
    {
3169
        return $this->eventQuery('name', 'ASC', WT_EVENTS_BIRT . '|' . WT_EVENTS_MARR . '|' . WT_EVENTS_DIV . '|' . WT_EVENTS_DEAT);
3170
    }
3171
3172
    /**
3173
     * Find the location of the earliest event.
3174
     *
3175
     * @return string
3176
     */
3177
    public function firstEventPlace()
3178
    {
3179
        return $this->eventQuery('place', 'ASC', WT_EVENTS_BIRT . '|' . WT_EVENTS_MARR . '|' . WT_EVENTS_DIV . '|' . WT_EVENTS_DEAT);
3180
    }
3181
3182
    /**
3183
     * Find the latest event.
3184
     *
3185
     * @return string
3186
     */
3187
    public function lastEvent()
3188
    {
3189
        return $this->eventQuery('full', 'DESC', WT_EVENTS_BIRT . '|' . WT_EVENTS_MARR . '|' . WT_EVENTS_DIV . '|' . WT_EVENTS_DEAT);
3190
    }
3191
3192
    /**
3193
     * Find the year of the latest event.
3194
     *
3195
     * @return string
3196
     */
3197
    public function lastEventYear()
3198
    {
3199
        return $this->eventQuery('year', 'DESC', WT_EVENTS_BIRT . '|' . WT_EVENTS_MARR . '|' . WT_EVENTS_DIV . '|' . WT_EVENTS_DEAT);
3200
    }
3201
3202
    /**
3203
     * Find the type of the latest event.
3204
     *
3205
     * @return string
3206
     */
3207
    public function lastEventType()
3208
    {
3209
        return $this->eventQuery('type', 'DESC', WT_EVENTS_BIRT . '|' . WT_EVENTS_MARR . '|' . WT_EVENTS_DIV . '|' . WT_EVENTS_DEAT);
3210
    }
3211
3212
    /**
3213
     * Find the name of the individual with the latest event.
3214
     *
3215
     * @return string
3216
     */
3217
    public function lastEventName()
3218
    {
3219
        return $this->eventQuery('name', 'DESC', WT_EVENTS_BIRT . '|' . WT_EVENTS_MARR . '|' . WT_EVENTS_DIV . '|' . WT_EVENTS_DEAT);
3220
    }
3221
3222
    /**
3223
     * FInd the location of the latest event.
3224
     *
3225
     * @return string
3226
     */
3227
    public function lastEventPlace()
3228
    {
3229
        return $this->eventQuery('place', 'DESC', WT_EVENTS_BIRT . '|' . WT_EVENTS_MARR . '|' . WT_EVENTS_DIV . '|' . WT_EVENTS_DEAT);
3230
    }
3231
3232
    /**
3233
     * Query the database for marriage tags.
3234
     *
3235
     * @param string $type
3236
     * @param string $age_dir
3237
     * @param string $sex
3238
     * @param bool   $show_years
3239
     *
3240
     * @return string
3241
     */
3242
    private function marriageQuery($type = 'full', $age_dir = 'ASC', $sex = 'F', $show_years = false)
3243
    {
3244
        if ($sex == 'F') {
3245
            $sex_field = 'f_wife';
3246
        } else {
3247
            $sex_field = 'f_husb';
3248
        }
3249
        if ($age_dir != 'ASC') {
3250
            $age_dir = 'DESC';
3251
        }
3252
        $rows = $this->runSql(
3253
            " SELECT fam.f_id AS famid, fam.{$sex_field}, married.d_julianday2-birth.d_julianday1 AS age, indi.i_id AS i_id" .
3254
            " FROM `##families` AS fam" .
3255
            " LEFT JOIN `##dates` AS birth ON birth.d_file = {$this->tree->getTreeId()}" .
3256
            " LEFT JOIN `##dates` AS married ON married.d_file = {$this->tree->getTreeId()}" .
3257
            " LEFT JOIN `##individuals` AS indi ON indi.i_file = {$this->tree->getTreeId()}" .
3258
            " WHERE" .
3259
            " birth.d_gid = indi.i_id AND" .
3260
            " married.d_gid = fam.f_id AND" .
3261
            " indi.i_id = fam.{$sex_field} AND" .
3262
            " fam.f_file = {$this->tree->getTreeId()} AND" .
3263
            " birth.d_fact = 'BIRT' AND" .
3264
            " married.d_fact = 'MARR' AND" .
3265
            " birth.d_julianday1 <> 0 AND" .
3266
            " married.d_julianday2 > birth.d_julianday1 AND" .
3267
            " i_sex='{$sex}'" .
3268
            " ORDER BY" .
3269
            " married.d_julianday2-birth.d_julianday1 {$age_dir} LIMIT 1"
3270
        );
3271
        if (!isset($rows[0])) {
3272
            return '';
3273
        }
3274
        $row = $rows[0];
3275
        if (isset($row['famid'])) {
3276
            $family = Family::getInstance($row['famid'], $this->tree);
3277
        }
3278
        if (isset($row['i_id'])) {
3279
            $person = Individual::getInstance($row['i_id'], $this->tree);
3280
        }
3281
        switch ($type) {
3282
            default:
3283
            case 'full':
3284
                if ($family->canShow()) {
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $family does not seem to be defined for all execution paths leading up to this point.
Loading history...
3285
                    $result = $family->formatList('span', false, $person->getFullName());
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $person does not seem to be defined for all execution paths leading up to this point.
Loading history...
3286
                } else {
3287
                    $result = I18N::translate('This information is private and cannot be shown.');
3288
                }
3289
                break;
3290
            case 'name':
3291
                $result = '<a href="' . $family->getHtmlUrl() . '">' . $person->getFullName() . '</a>';
3292
                break;
3293
            case 'age':
3294
                $age = $row['age'];
3295
                if ($show_years) {
3296
                    if ((int) ($age / 365.25) > 0) {
3297
                        $age = (int) ($age / 365.25) . 'y';
3298
                    } elseif ((int) ($age / 30.4375) > 0) {
3299
                        $age = (int) ($age / 30.4375) . 'm';
3300
                    } else {
3301
                        $age = $age . 'd';
3302
                    }
3303
                    $result = FunctionsDate::getAgeAtEvent($age);
3304
                } else {
3305
                    $result = I18N::number((int) ($age / 365.25));
3306
                }
3307
                break;
3308
        }
3309
3310
        return $result;
3311
    }
3312
3313
    /**
3314
     * General query on age at marriage.
3315
     *
3316
     * @param string   $type
3317
     * @param string   $age_dir
3318
     * @param string[] $params
3319
     *
3320
     * @return string
3321
     */
3322
    private function ageOfMarriageQuery($type = 'list', $age_dir = 'ASC', $params = array())
3323
    {
3324
        if (isset($params[0])) {
3325
            $total = (int) $params[0];
3326
        } else {
3327
            $total = 10;
3328
        }
3329
        if ($age_dir != 'ASC') {
3330
            $age_dir = 'DESC';
3331
        }
3332
        $hrows = $this->runSql(
3333
            " SELECT DISTINCT fam.f_id AS family, MIN(husbdeath.d_julianday2-married.d_julianday1) AS age" .
3334
            " FROM `##families` AS fam" .
3335
            " LEFT JOIN `##dates` AS married ON married.d_file = {$this->tree->getTreeId()}" .
3336
            " LEFT JOIN `##dates` AS husbdeath ON husbdeath.d_file = {$this->tree->getTreeId()}" .
3337
            " WHERE" .
3338
            " fam.f_file = {$this->tree->getTreeId()} AND" .
3339
            " husbdeath.d_gid = fam.f_husb AND" .
3340
            " husbdeath.d_fact = 'DEAT' AND" .
3341
            " married.d_gid = fam.f_id AND" .
3342
            " married.d_fact = 'MARR' AND" .
3343
            " married.d_julianday1 < husbdeath.d_julianday2 AND" .
3344
            " married.d_julianday1 <> 0" .
3345
            " GROUP BY family" .
3346
            " ORDER BY age {$age_dir}");
3347
        $wrows = $this->runSql(
3348
            " SELECT DISTINCT fam.f_id AS family, MIN(wifedeath.d_julianday2-married.d_julianday1) AS age" .
3349
            " FROM `##families` AS fam" .
3350
            " LEFT JOIN `##dates` AS married ON married.d_file = {$this->tree->getTreeId()}" .
3351
            " LEFT JOIN `##dates` AS wifedeath ON wifedeath.d_file = {$this->tree->getTreeId()}" .
3352
            " WHERE" .
3353
            " fam.f_file = {$this->tree->getTreeId()} AND" .
3354
            " wifedeath.d_gid = fam.f_wife AND" .
3355
            " wifedeath.d_fact = 'DEAT' AND" .
3356
            " married.d_gid = fam.f_id AND" .
3357
            " married.d_fact = 'MARR' AND" .
3358
            " married.d_julianday1 < wifedeath.d_julianday2 AND" .
3359
            " married.d_julianday1 <> 0" .
3360
            " GROUP BY family" .
3361
            " ORDER BY age {$age_dir}");
3362
        $drows = $this->runSql(
3363
            " SELECT DISTINCT fam.f_id AS family, MIN(divorced.d_julianday2-married.d_julianday1) AS age" .
3364
            " FROM `##families` AS fam" .
3365
            " LEFT JOIN `##dates` AS married ON married.d_file = {$this->tree->getTreeId()}" .
3366
            " LEFT JOIN `##dates` AS divorced ON divorced.d_file = {$this->tree->getTreeId()}" .
3367
            " WHERE" .
3368
            " fam.f_file = {$this->tree->getTreeId()} AND" .
3369
            " married.d_gid = fam.f_id AND" .
3370
            " married.d_fact = 'MARR' AND" .
3371
            " divorced.d_gid = fam.f_id AND" .
3372
            " divorced.d_fact IN ('DIV', 'ANUL', '_SEPR', '_DETS') AND" .
3373
            " married.d_julianday1 < divorced.d_julianday2 AND" .
3374
            " married.d_julianday1 <> 0" .
3375
            " GROUP BY family" .
3376
            " ORDER BY age {$age_dir}");
3377
        if (!isset($hrows) && !isset($wrows) && !isset($drows)) {
3378
            return '';
3379
        }
3380
        $rows = array();
3381
        foreach ($drows as $family) {
3382
            $rows[$family['family']] = $family['age'];
3383
        }
3384
        foreach ($hrows as $family) {
3385
            if (!isset($rows[$family['family']])) {
3386
                $rows[$family['family']] = $family['age'];
3387
            }
3388
        }
3389
        foreach ($wrows as $family) {
3390
            if (!isset($rows[$family['family']])) {
3391
                $rows[$family['family']] = $family['age'];
3392
            } elseif ($rows[$family['family']] > $family['age']) {
3393
                $rows[$family['family']] = $family['age'];
3394
            }
3395
        }
3396
        if ($age_dir === 'DESC') {
3397
            arsort($rows);
3398
        } else {
3399
            asort($rows);
3400
        }
3401
        $top10 = array();
3402
        $i     = 0;
3403
        foreach ($rows as $fam => $age) {
3404
            $family = Family::getInstance($fam, $this->tree);
3405
            if ($type === 'name') {
3406
                return $family->formatList('span', false, $family->getFullName());
3407
            }
3408
            if ((int) ($age / 365.25) > 0) {
3409
                $age = (int) ($age / 365.25) . 'y';
3410
            } elseif ((int) ($age / 30.4375) > 0) {
3411
                $age = (int) ($age / 30.4375) . 'm';
3412
            } else {
3413
                $age = $age . 'd';
3414
            }
3415
            $age = FunctionsDate::getAgeAtEvent($age);
3416
            if ($type === 'age') {
3417
                return $age;
3418
            }
3419
            $husb = $family->getHusband();
0 ignored issues
show
Bug introduced by
The method getHusband() does not exist on Fisharebest\Webtrees\Individual. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

3419
            /** @scrutinizer ignore-call */ 
3420
            $husb = $family->getHusband();

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
Bug introduced by
The method getHusband() does not exist on Fisharebest\Webtrees\Repository. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

3419
            /** @scrutinizer ignore-call */ 
3420
            $husb = $family->getHusband();

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
Bug introduced by
The method getHusband() does not exist on Fisharebest\Webtrees\Media. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

3419
            /** @scrutinizer ignore-call */ 
3420
            $husb = $family->getHusband();

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
Bug introduced by
The method getHusband() does not exist on Fisharebest\Webtrees\GedcomRecord. It seems like you code against a sub-type of Fisharebest\Webtrees\GedcomRecord such as Fisharebest\Webtrees\Family. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

3419
            /** @scrutinizer ignore-call */ 
3420
            $husb = $family->getHusband();
Loading history...
Bug introduced by
The method getHusband() does not exist on Fisharebest\Webtrees\Note. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

3419
            /** @scrutinizer ignore-call */ 
3420
            $husb = $family->getHusband();

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
Bug introduced by
The method getHusband() does not exist on Fisharebest\Webtrees\Source. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

3419
            /** @scrutinizer ignore-call */ 
3420
            $husb = $family->getHusband();

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
3420
            $wife = $family->getWife();
0 ignored issues
show
Bug introduced by
The method getWife() does not exist on Fisharebest\Webtrees\GedcomRecord. It seems like you code against a sub-type of Fisharebest\Webtrees\GedcomRecord such as Fisharebest\Webtrees\Family. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

3420
            /** @scrutinizer ignore-call */ 
3421
            $wife = $family->getWife();
Loading history...
Bug introduced by
The method getWife() does not exist on Fisharebest\Webtrees\Individual. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

3420
            /** @scrutinizer ignore-call */ 
3421
            $wife = $family->getWife();

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
Bug introduced by
The method getWife() does not exist on Fisharebest\Webtrees\Source. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

3420
            /** @scrutinizer ignore-call */ 
3421
            $wife = $family->getWife();

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
Bug introduced by
The method getWife() does not exist on Fisharebest\Webtrees\Note. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

3420
            /** @scrutinizer ignore-call */ 
3421
            $wife = $family->getWife();

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
Bug introduced by
The method getWife() does not exist on Fisharebest\Webtrees\Repository. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

3420
            /** @scrutinizer ignore-call */ 
3421
            $wife = $family->getWife();

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
Bug introduced by
The method getWife() does not exist on Fisharebest\Webtrees\Media. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

3420
            /** @scrutinizer ignore-call */ 
3421
            $wife = $family->getWife();

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
3421
            if ($husb && $wife && ($husb->getAllDeathDates() && $wife->getAllDeathDates() || !$husb->isDead() || !$wife->isDead())) {
3422
                if ($family->canShow()) {
3423
                    if ($type === 'list') {
3424
                        $top10[] = "<li><a href=\"" . $family->getHtmlUrl() . "\">" . $family->getFullName() . "</a> (" . $age . ")" . "</li>";
3425
                    } else {
3426
                        $top10[] = "<a href=\"" . $family->getHtmlUrl() . "\">" . $family->getFullName() . "</a> (" . $age . ")";
3427
                    }
3428
                }
3429
                if (++$i === $total) {
3430
                    break;
3431
                }
3432
            }
3433
        }
3434
        if ($type === 'list') {
3435
            $top10 = implode('', $top10);
3436
        } else {
3437
            $top10 = implode('; ', $top10);
3438
        }
3439
        if (I18N::direction() === 'rtl') {
3440
            $top10 = str_replace(array('[', ']', '(', ')', '+'), array('&rlm;[', '&rlm;]', '&rlm;(', '&rlm;)', '&rlm;+'), $top10);
3441
        }
3442
        if ($type === 'list') {
3443
            return '<ul>' . $top10 . '</ul>';
3444
        }
3445
3446
        return $top10;
3447
    }
3448
3449
    /**
3450
     * Find the ages between spouses.
3451
     *
3452
     * @param string   $type
3453
     * @param string   $age_dir
3454
     * @param string[] $params
3455
     *
3456
     * @return string
3457
     */
3458
    private function ageBetweenSpousesQuery($type = 'list', $age_dir = 'DESC', $params = array())
3459
    {
3460
        if (isset($params[0])) {
3461
            $total = (int) $params[0];
3462
        } else {
3463
            $total = 10;
3464
        }
3465
        if ($age_dir === 'DESC') {
3466
            $sql =
3467
                "SELECT f_id AS xref, MIN(wife.d_julianday2-husb.d_julianday1) AS age" .
3468
                " FROM `##families`" .
3469
                " JOIN `##dates` AS wife ON wife.d_gid = f_wife AND wife.d_file = f_file" .
3470
                " JOIN `##dates` AS husb ON husb.d_gid = f_husb AND husb.d_file = f_file" .
3471
                " WHERE f_file = :tree_id" .
3472
                " AND husb.d_fact = 'BIRT'" .
3473
                " AND wife.d_fact = 'BIRT'" .
3474
                " AND wife.d_julianday2 >= husb.d_julianday1 AND husb.d_julianday1 <> 0" .
3475
                " GROUP BY xref" .
3476
                " ORDER BY age DESC" .
3477
                " LIMIT :limit";
3478
        } else {
3479
            $sql =
3480
                "SELECT f_id AS xref, MIN(husb.d_julianday2-wife.d_julianday1) AS age" .
3481
                " FROM `##families`" .
3482
                " JOIN `##dates` AS wife ON wife.d_gid = f_wife AND wife.d_file = f_file" .
3483
                " JOIN `##dates` AS husb ON husb.d_gid = f_husb AND husb.d_file = f_file" .
3484
                " WHERE f_file = :tree_id" .
3485
                " AND husb.d_fact = 'BIRT'" .
3486
                " AND wife.d_fact = 'BIRT'" .
3487
                " AND husb.d_julianday2 >= wife.d_julianday1 AND wife.d_julianday1 <> 0" .
3488
                " GROUP BY xref" .
3489
                " ORDER BY age DESC" .
3490
                " LIMIT :limit";
3491
        }
3492
        $rows = Database::prepare(
3493
            $sql
3494
        )->execute(array(
3495
            'tree_id' => $this->tree->getTreeId(),
3496
            'limit'   => $total,
3497
        ))->fetchAll();
3498
3499
        $top10 = array();
3500
        foreach ($rows as $fam) {
3501
            $family = Family::getInstance($fam->xref, $this->tree);
3502
            if ($fam->age < 0) {
3503
                break;
3504
            }
3505
            $age = $fam->age;
3506
            if ((int) ($age / 365.25) > 0) {
3507
                $age = (int) ($age / 365.25) . 'y';
3508
            } elseif ((int) ($age / 30.4375) > 0) {
3509
                $age = (int) ($age / 30.4375) . 'm';
3510
            } else {
3511
                $age = $age . 'd';
3512
            }
3513
            $age = FunctionsDate::getAgeAtEvent($age);
3514
            if ($family->canShow()) {
3515
                if ($type === 'list') {
3516
                    $top10[] = '<li><a href="' . $family->getHtmlUrl() . '">' . $family->getFullName() . '</a> (' . $age . ')' . "</li>";
3517
                } else {
3518
                    $top10[] = '<a href="' . $family->getHtmlUrl() . '">' . $family->getFullName() . '</a> (' . $age . ')';
3519
                }
3520
            }
3521
        }
3522
        if ($type === 'list') {
3523
            $top10 = implode('', $top10);
3524
            if ($top10) {
3525
                $top10 = '<ul>' . $top10 . '</ul>';
3526
            }
3527
        } else {
3528
            $top10 = implode(' ', $top10);
3529
        }
3530
3531
        return $top10;
3532
    }
3533
3534
    /**
3535
     * General query on parents.
3536
     *
3537
     * @param string $type
3538
     * @param string $age_dir
3539
     * @param string $sex
3540
     * @param bool   $show_years
3541
     *
3542
     * @return string
3543
     */
3544
    private function parentsQuery($type = 'full', $age_dir = 'ASC', $sex = 'F', $show_years = false)
3545
    {
3546
        if ($sex == 'F') {
3547
            $sex_field = 'WIFE';
3548
        } else {
3549
            $sex_field = 'HUSB';
3550
        }
3551
        if ($age_dir != 'ASC') {
3552
            $age_dir = 'DESC';
3553
        }
3554
        $rows = $this->runSql(
3555
            " SELECT" .
3556
            " parentfamily.l_to AS id," .
3557
            " childbirth.d_julianday2-birth.d_julianday1 AS age" .
3558
            " FROM `##link` AS parentfamily" .
3559
            " JOIN `##link` AS childfamily ON childfamily.l_file = {$this->tree->getTreeId()}" .
3560
            " JOIN `##dates` AS birth ON birth.d_file = {$this->tree->getTreeId()}" .
3561
            " JOIN `##dates` AS childbirth ON childbirth.d_file = {$this->tree->getTreeId()}" .
3562
            " WHERE" .
3563
            " birth.d_gid = parentfamily.l_to AND" .
3564
            " childfamily.l_to = childbirth.d_gid AND" .
3565
            " childfamily.l_type = 'CHIL' AND" .
3566
            " parentfamily.l_type = '{$sex_field}' AND" .
3567
            " childfamily.l_from = parentfamily.l_from AND" .
3568
            " parentfamily.l_file = {$this->tree->getTreeId()} AND" .
3569
            " birth.d_fact = 'BIRT' AND" .
3570
            " childbirth.d_fact = 'BIRT' AND" .
3571
            " birth.d_julianday1 <> 0 AND" .
3572
            " childbirth.d_julianday2 > birth.d_julianday1" .
3573
            " ORDER BY age {$age_dir} LIMIT 1"
3574
        );
3575
        if (!isset($rows[0])) {
3576
            return '';
3577
        }
3578
        $row = $rows[0];
3579
        if (isset($row['id'])) {
3580
            $person = Individual::getInstance($row['id'], $this->tree);
3581
        }
3582
        switch ($type) {
3583
            default:
3584
            case 'full':
3585
                if ($person->canShow()) {
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $person does not seem to be defined for all execution paths leading up to this point.
Loading history...
3586
                    $result = $person->formatList('span', false, $person->getFullName());
3587
                } else {
3588
                    $result = I18N::translate('This information is private and cannot be shown.');
3589
                }
3590
                break;
3591
            case 'name':
3592
                $result = '<a href="' . $person->getHtmlUrl() . '">' . $person->getFullName() . '</a>';
3593
                break;
3594
            case 'age':
3595
                $age = $row['age'];
3596
                if ($show_years) {
3597
                    if ((int) ($age / 365.25) > 0) {
3598
                        $age = (int) ($age / 365.25) . 'y';
3599
                    } elseif ((int) ($age / 30.4375) > 0) {
3600
                        $age = (int) ($age / 30.4375) . 'm';
3601
                    } else {
3602
                        $age = $age . 'd';
3603
                    }
3604
                    $result = FunctionsDate::getAgeAtEvent($age);
3605
                } else {
3606
                    $result = (int) ($age / 365.25);
3607
                }
3608
                break;
3609
        }
3610
3611
        return $result;
3612
    }
3613
3614
    /**
3615
     * General query on marriages.
3616
     *
3617
     * @param bool     $simple
3618
     * @param bool     $first
3619
     * @param int      $year1
3620
     * @param int      $year2
3621
     * @param string[] $params
3622
     *
3623
     * @return string|array
3624
     */
3625
    public function statsMarrQuery($simple = true, $first = false, $year1 = -1, $year2 = -1, $params = array())
3626
    {
3627
        $WT_STATS_CHART_COLOR1 = Theme::theme()->parameter('distribution-chart-no-values');
3628
        $WT_STATS_CHART_COLOR2 = Theme::theme()->parameter('distribution-chart-high-values');
3629
        $WT_STATS_S_CHART_X    = Theme::theme()->parameter('stats-small-chart-x');
3630
        $WT_STATS_S_CHART_Y    = Theme::theme()->parameter('stats-small-chart-y');
3631
3632
        if ($simple) {
3633
            $sql =
3634
                "SELECT FLOOR(d_year/100+1) AS century, COUNT(*) AS total" .
3635
                " FROM `##dates`" .
3636
                " WHERE d_file={$this->tree->getTreeId()} AND d_year<>0 AND d_fact='MARR' AND d_type IN ('@#DGREGORIAN@', '@#DJULIAN@')";
3637
            if ($year1 >= 0 && $year2 >= 0) {
3638
                $sql .= " AND d_year BETWEEN '{$year1}' AND '{$year2}'";
3639
            }
3640
            $sql .= " GROUP BY century ORDER BY century";
3641
        } elseif ($first) {
3642
            $years = '';
3643
            if ($year1 >= 0 && $year2 >= 0) {
3644
                $years = " married.d_year BETWEEN '{$year1}' AND '{$year2}' AND";
3645
            }
3646
            $sql =
3647
                " SELECT fam.f_id AS fams, fam.f_husb, fam.f_wife, married.d_julianday2 AS age, married.d_month AS month, indi.i_id AS indi" .
3648
                " FROM `##families` AS fam" .
3649
                " LEFT JOIN `##dates` AS married ON married.d_file = {$this->tree->getTreeId()}" .
3650
                " LEFT JOIN `##individuals` AS indi ON indi.i_file = {$this->tree->getTreeId()}" .
3651
                " WHERE" .
3652
                " married.d_gid = fam.f_id AND" .
3653
                " fam.f_file = {$this->tree->getTreeId()} AND" .
3654
                " married.d_fact = 'MARR' AND" .
3655
                " married.d_julianday2 <> 0 AND" .
3656
                $years .
3657
                " (indi.i_id = fam.f_husb OR indi.i_id = fam.f_wife)" .
3658
                " ORDER BY fams, indi, age ASC";
3659
        } else {
3660
            $sql =
3661
                "SELECT d_month, COUNT(*) AS total" .
3662
                " FROM `##dates`" .
3663
                " WHERE d_file={$this->tree->getTreeId()} AND d_fact='MARR'";
3664
            if ($year1 >= 0 && $year2 >= 0) {
3665
                $sql .= " AND d_year BETWEEN '{$year1}' AND '{$year2}'";
3666
            }
3667
            $sql .= " GROUP BY d_month";
3668
        }
3669
        $rows = $this->runSql($sql);
3670
        if (!isset($rows)) {
3671
            return '';
3672
        }
3673
        if ($simple) {
3674
            if (isset($params[0]) && $params[0] != '') {
3675
                $size = strtolower($params[0]);
3676
            } else {
3677
                $size = $WT_STATS_S_CHART_X . "x" . $WT_STATS_S_CHART_Y;
3678
            }
3679
            if (isset($params[1]) && $params[1] != '') {
3680
                $color_from = strtolower($params[1]);
3681
            } else {
3682
                $color_from = $WT_STATS_CHART_COLOR1;
3683
            }
3684
            if (isset($params[2]) && $params[2] != '') {
3685
                $color_to = strtolower($params[2]);
3686
            } else {
3687
                $color_to = $WT_STATS_CHART_COLOR2;
3688
            }
3689
            $sizes = explode('x', $size);
3690
            $tot   = 0;
3691
            foreach ($rows as $values) {
3692
                $tot += (int) $values['total'];
3693
            }
3694
            // Beware divide by zero
3695
            if ($tot === 0) {
0 ignored issues
show
introduced by
The condition $tot === 0 is always true.
Loading history...
3696
                return '';
3697
            }
3698
            $centuries = '';
3699
            $counts    = array();
3700
            foreach ($rows as $values) {
3701
                $counts[] = round(100 * $values['total'] / $tot, 0);
3702
                $centuries .= $this->centuryName($values['century']) . ' - ' . I18N::number($values['total']) . '|';
3703
            }
3704
            $chd = $this->arrayToExtendedEncoding($counts);
3705
            $chl = substr($centuries, 0, -1);
3706
3707
            return "<img src=\"https://chart.googleapis.com/chart?cht=p3&amp;chd=e:{$chd}&amp;chs={$size}&amp;chco={$color_from},{$color_to}&amp;chf=bg,s,ffffff00&amp;chl={$chl}\" width=\"{$sizes[0]}\" height=\"{$sizes[1]}\" alt=\"" . I18N::translate('Marriages by century') . "\" title=\"" . I18N::translate('Marriages by century') . "\" />";
3708
        }
3709
3710
        return $rows;
3711
    }
3712
3713
    /**
3714
     * General query on divorces.
3715
     *
3716
     * @param bool     $simple
3717
     * @param bool     $first
3718
     * @param int      $year1
3719
     * @param int      $year2
3720
     * @param string[] $params
3721
     *
3722
     * @return string|array
3723
     */
3724
    private function statsDivQuery($simple = true, $first = false, $year1 = -1, $year2 = -1, $params = array())
3725
    {
3726
        $WT_STATS_CHART_COLOR1 = Theme::theme()->parameter('distribution-chart-no-values');
3727
        $WT_STATS_CHART_COLOR2 = Theme::theme()->parameter('distribution-chart-high-values');
3728
        $WT_STATS_S_CHART_X    = Theme::theme()->parameter('stats-small-chart-x');
3729
        $WT_STATS_S_CHART_Y    = Theme::theme()->parameter('stats-small-chart-y');
3730
3731
        if ($simple) {
3732
            $sql =
3733
                "SELECT FLOOR(d_year/100+1) AS century, COUNT(*) AS total" .
3734
                " FROM `##dates`" .
3735
                " WHERE d_file={$this->tree->getTreeId()} AND d_year<>0 AND d_fact = 'DIV' AND d_type IN ('@#DGREGORIAN@', '@#DJULIAN@')";
3736
            if ($year1 >= 0 && $year2 >= 0) {
3737
                $sql .= " AND d_year BETWEEN '{$year1}' AND '{$year2}'";
3738
            }
3739
            $sql .= " GROUP BY century ORDER BY century";
3740
        } elseif ($first) {
3741
            $years = '';
3742
            if ($year1 >= 0 && $year2 >= 0) {
3743
                $years = " divorced.d_year BETWEEN '{$year1}' AND '{$year2}' AND";
3744
            }
3745
            $sql =
3746
                " SELECT fam.f_id AS fams, fam.f_husb, fam.f_wife, divorced.d_julianday2 AS age, divorced.d_month AS month, indi.i_id AS indi" .
3747
                " FROM `##families` AS fam" .
3748
                " LEFT JOIN `##dates` AS divorced ON divorced.d_file = {$this->tree->getTreeId()}" .
3749
                " LEFT JOIN `##individuals` AS indi ON indi.i_file = {$this->tree->getTreeId()}" .
3750
                " WHERE" .
3751
                " divorced.d_gid = fam.f_id AND" .
3752
                " fam.f_file = {$this->tree->getTreeId()} AND" .
3753
                " divorced.d_fact = 'DIV' AND" .
3754
                " divorced.d_julianday2 <> 0 AND" .
3755
                $years .
3756
                " (indi.i_id = fam.f_husb OR indi.i_id = fam.f_wife)" .
3757
                " ORDER BY fams, indi, age ASC";
3758
        } else {
3759
            $sql =
3760
                "SELECT d_month, COUNT(*) AS total FROM `##dates` " .
3761
                "WHERE d_file={$this->tree->getTreeId()} AND d_fact = 'DIV'";
3762
            if ($year1 >= 0 && $year2 >= 0) {
3763
                $sql .= " AND d_year BETWEEN '{$year1}' AND '{$year2}'";
3764
            }
3765
            $sql .= " GROUP BY d_month";
3766
        }
3767
        $rows = $this->runSql($sql);
3768
        if (!isset($rows)) {
3769
            return '';
3770
        }
3771
        if ($simple) {
3772
            if (isset($params[0]) && $params[0] != '') {
3773
                $size = strtolower($params[0]);
3774
            } else {
3775
                $size = $WT_STATS_S_CHART_X . "x" . $WT_STATS_S_CHART_Y;
3776
            }
3777
            if (isset($params[1]) && $params[1] != '') {
3778
                $color_from = strtolower($params[1]);
3779
            } else {
3780
                $color_from = $WT_STATS_CHART_COLOR1;
3781
            }
3782
            if (isset($params[2]) && $params[2] != '') {
3783
                $color_to = strtolower($params[2]);
3784
            } else {
3785
                $color_to = $WT_STATS_CHART_COLOR2;
3786
            }
3787
            $sizes = explode('x', $size);
3788
            $tot   = 0;
3789
            foreach ($rows as $values) {
3790
                $tot += (int) $values['total'];
3791
            }
3792
            // Beware divide by zero
3793
            if ($tot === 0) {
0 ignored issues
show
introduced by
The condition $tot === 0 is always true.
Loading history...
3794
                return '';
3795
            }
3796
            $centuries = '';
3797
            $counts    = array();
3798
            foreach ($rows as $values) {
3799
                $counts[] = round(100 * $values['total'] / $tot, 0);
3800
                $centuries .= $this->centuryName($values['century']) . ' - ' . I18N::number($values['total']) . '|';
3801
            }
3802
            $chd = $this->arrayToExtendedEncoding($counts);
3803
            $chl = substr($centuries, 0, -1);
3804
3805
            return "<img src=\"https://chart.googleapis.com/chart?cht=p3&amp;chd=e:{$chd}&amp;chs={$size}&amp;chco={$color_from},{$color_to}&amp;chf=bg,s,ffffff00&amp;chl={$chl}\" width=\"{$sizes[0]}\" height=\"{$sizes[1]}\" alt=\"" . I18N::translate('Divorces by century') . "\" title=\"" . I18N::translate('Divorces by century') . "\" />";
3806
        }
3807
3808
        return $rows;
3809
    }
3810
3811
    /**
3812
     * Find the earliest marriage.
3813
     *
3814
     * @return string
3815
     */
3816
    public function firstMarriage()
3817
    {
3818
        return $this->mortalityQuery('full', 'ASC', 'MARR');
3819
    }
3820
3821
    /**
3822
     * Find the year of the earliest marriage.
3823
     *
3824
     * @return string
3825
     */
3826
    public function firstMarriageYear()
3827
    {
3828
        return $this->mortalityQuery('year', 'ASC', 'MARR');
3829
    }
3830
3831
    /**
3832
     * Find the names of spouses of the earliest marriage.
3833
     *
3834
     * @return string
3835
     */
3836
    public function firstMarriageName()
3837
    {
3838
        return $this->mortalityQuery('name', 'ASC', 'MARR');
3839
    }
3840
3841
    /**
3842
     * Find the place of the earliest marriage.
3843
     *
3844
     * @return string
3845
     */
3846
    public function firstMarriagePlace()
3847
    {
3848
        return $this->mortalityQuery('place', 'ASC', 'MARR');
3849
    }
3850
3851
    /**
3852
     * Find the latest marriage.
3853
     *
3854
     * @return string
3855
     */
3856
    public function lastMarriage()
3857
    {
3858
        return $this->mortalityQuery('full', 'DESC', 'MARR');
3859
    }
3860
3861
    /**
3862
     * Find the year of the latest marriage.
3863
     *
3864
     * @return string
3865
     */
3866
    public function lastMarriageYear()
3867
    {
3868
        return $this->mortalityQuery('year', 'DESC', 'MARR');
3869
    }
3870
3871
    /**
3872
     * Find the names of spouses of the latest marriage.
3873
     *
3874
     * @return string
3875
     */
3876
    public function lastMarriageName()
3877
    {
3878
        return $this->mortalityQuery('name', 'DESC', 'MARR');
3879
    }
3880
3881
    /**
3882
     * Find the location of the latest marriage.
3883
     *
3884
     * @return string
3885
     */
3886
    public function lastMarriagePlace()
3887
    {
3888
        return $this->mortalityQuery('place', 'DESC', 'MARR');
3889
    }
3890
3891
    /**
3892
     * General query on marriages.
3893
     *
3894
     * @param string[] $params
3895
     *
3896
     * @return string
3897
     */
3898
    public function statsMarr($params = array())
3899
    {
3900
        return $this->statsMarrQuery(true, false, -1, -1, $params);
3901
    }
3902
3903
    /**
3904
     * Find the earliest divorce.
3905
     *
3906
     * @return string
3907
     */
3908
    public function firstDivorce()
3909
    {
3910
        return $this->mortalityQuery('full', 'ASC', 'DIV');
3911
    }
3912
3913
    /**
3914
     * Find the year of the earliest divorce.
3915
     *
3916
     * @return string
3917
     */
3918
    public function firstDivorceYear()
3919
    {
3920
        return $this->mortalityQuery('year', 'ASC', 'DIV');
3921
    }
3922
3923
    /**
3924
     * Find the names of individuals in the earliest divorce.
3925
     *
3926
     * @return string
3927
     */
3928
    public function firstDivorceName()
3929
    {
3930
        return $this->mortalityQuery('name', 'ASC', 'DIV');
3931
    }
3932
3933
    /**
3934
     * Find the location of the earliest divorce.
3935
     *
3936
     * @return string
3937
     */
3938
    public function firstDivorcePlace()
3939
    {
3940
        return $this->mortalityQuery('place', 'ASC', 'DIV');
3941
    }
3942
3943
    /**
3944
     * Find the latest divorce.
3945
     *
3946
     * @return string
3947
     */
3948
    public function lastDivorce()
3949
    {
3950
        return $this->mortalityQuery('full', 'DESC', 'DIV');
3951
    }
3952
3953
    /**
3954
     * Find the year of the latest divorce.
3955
     *
3956
     * @return string
3957
     */
3958
    public function lastDivorceYear()
3959
    {
3960
        return $this->mortalityQuery('year', 'DESC', 'DIV');
3961
    }
3962
3963
    /**
3964
     * Find the names of the individuals in the latest divorce.
3965
     *
3966
     * @return string
3967
     */
3968
    public function lastDivorceName()
3969
    {
3970
        return $this->mortalityQuery('name', 'DESC', 'DIV');
3971
    }
3972
3973
    /**
3974
     * Find the location of the latest divorce.
3975
     *
3976
     * @return string
3977
     */
3978
    public function lastDivorcePlace()
3979
    {
3980
        return $this->mortalityQuery('place', 'DESC', 'DIV');
3981
    }
3982
3983
    /**
3984
     * General divorce query.
3985
     *
3986
     * @param string[] $params
3987
     *
3988
     * @return string
3989
     */
3990
    public function statsDiv($params = array())
3991
    {
3992
        return $this->statsDivQuery(true, false, -1, -1, $params);
3993
    }
3994
3995
    /**
3996
     * General query on ages at marriage.
3997
     *
3998
     * @param bool     $simple
3999
     * @param string   $sex
4000
     * @param int      $year1
4001
     * @param int      $year2
4002
     * @param string[] $params
4003
     *
4004
     * @return array|string
4005
     */
4006
    public function statsMarrAgeQuery($simple = true, $sex = 'M', $year1 = -1, $year2 = -1, $params = array())
4007
    {
4008
        if ($simple) {
4009
            if (isset($params[0]) && $params[0] != '') {
4010
                $size = strtolower($params[0]);
4011
            } else {
4012
                $size = '200x250';
4013
            }
4014
            $sizes = explode('x', $size);
4015
            $rows  = $this->runSql(
4016
                "SELECT " .
4017
                " ROUND(AVG(married.d_julianday2-birth.d_julianday1-182.5)/365.25,1) AS age, " .
4018
                " FLOOR(married.d_year/100+1) AS century, " .
4019
                " 'M' AS sex " .
4020
                "FROM `##dates` AS married " .
4021
                "JOIN `##families` AS fam ON (married.d_gid=fam.f_id AND married.d_file=fam.f_file) " .
4022
                "JOIN `##dates` AS birth ON (birth.d_gid=fam.f_husb AND birth.d_file=fam.f_file) " .
4023
                "WHERE " .
4024
                " '{$sex}' IN ('M', 'BOTH') AND " .
4025
                " married.d_file={$this->tree->getTreeId()} AND married.d_type IN ('@#DGREGORIAN@', '@#DJULIAN@') AND married.d_fact='MARR' AND " .
4026
                " birth.d_type IN ('@#DGREGORIAN@', '@#DJULIAN@') AND birth.d_fact='BIRT' AND " .
4027
                " married.d_julianday1>birth.d_julianday1 AND birth.d_julianday1<>0 " .
4028
                "GROUP BY century, sex " .
4029
                "UNION ALL " .
4030
                "SELECT " .
4031
                " ROUND(AVG(married.d_julianday2-birth.d_julianday1-182.5)/365.25,1) AS age, " .
4032
                " FLOOR(married.d_year/100+1) AS century, " .
4033
                " 'F' AS sex " .
4034
                "FROM `##dates` AS married " .
4035
                "JOIN `##families` AS fam ON (married.d_gid=fam.f_id AND married.d_file=fam.f_file) " .
4036
                "JOIN `##dates` AS birth ON (birth.d_gid=fam.f_wife AND birth.d_file=fam.f_file) " .
4037
                "WHERE " .
4038
                " '{$sex}' IN ('F', 'BOTH') AND " .
4039
                " married.d_file={$this->tree->getTreeId()} AND married.d_type IN ('@#DGREGORIAN@', '@#DJULIAN@') AND married.d_fact='MARR' AND " .
4040
                " birth.d_type IN ('@#DGREGORIAN@', '@#DJULIAN@') AND birth.d_fact='BIRT' AND " .
4041
                " married.d_julianday1>birth.d_julianday1 AND birth.d_julianday1<>0 " .
4042
                " GROUP BY century, sex ORDER BY century"
4043
            );
4044
            if (empty($rows)) {
4045
                return '';
4046
            }
4047
            $max = 0;
4048
            foreach ($rows as $values) {
4049
                if ($max < $values['age']) {
4050
                    $max = $values['age'];
4051
                }
4052
            }
4053
            $chxl    = '0:|';
4054
            $chmm    = '';
4055
            $chmf    = '';
4056
            $i       = 0;
4057
            $countsm = '';
4058
            $countsf = '';
4059
            $countsa = '';
4060
            $out     = array();
4061
            foreach ($rows as $values) {
4062
                $out[$values['century']][$values['sex']] = $values['age'];
4063
            }
4064
            foreach ($out as $century => $values) {
4065
                if ($sizes[0] < 1000) {
4066
                    $sizes[0] += 50;
4067
                }
4068
                $chxl .= $this->centuryName($century) . '|';
4069
                $average = 0;
4070
                if (isset($values['F'])) {
4071
                    if ($max <= 50) {
4072
                        $value = $values['F'] * 2;
4073
                    } else {
4074
                        $value = $values['F'];
4075
                    }
4076
                    $countsf .= $value . ',';
4077
                    $average = $value;
4078
                    $chmf .= 't' . $values['F'] . ',000000,1,' . $i . ',11,1|';
4079
                } else {
4080
                    $countsf .= '0,';
4081
                    $chmf .= 't0,000000,1,' . $i . ',11,1|';
4082
                }
4083
                if (isset($values['M'])) {
4084
                    if ($max <= 50) {
4085
                        $value = $values['M'] * 2;
4086
                    } else {
4087
                        $value = $values['M'];
4088
                    }
4089
                    $countsm .= $value . ',';
4090
                    if ($average == 0) {
4091
                        $countsa .= $value . ',';
4092
                    } else {
4093
                        $countsa .= (($value + $average) / 2) . ',';
4094
                    }
4095
                    $chmm .= 't' . $values['M'] . ',000000,0,' . $i . ',11,1|';
4096
                } else {
4097
                    $countsm .= '0,';
4098
                    if ($average == 0) {
4099
                        $countsa .= '0,';
4100
                    } else {
4101
                        $countsa .= $value . ',';
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $value does not seem to be defined for all execution paths leading up to this point.
Loading history...
4102
                    }
4103
                    $chmm .= 't0,000000,0,' . $i . ',11,1|';
4104
                }
4105
                $i++;
4106
            }
4107
            $countsm = substr($countsm, 0, -1);
4108
            $countsf = substr($countsf, 0, -1);
4109
            $countsa = substr($countsa, 0, -1);
4110
            $chmf    = substr($chmf, 0, -1);
4111
            $chd     = 't2:' . $countsm . '|' . $countsf . '|' . $countsa;
4112
            if ($max <= 50) {
4113
                $chxl .= '1:||' . I18N::translate('century') . '|2:|0|10|20|30|40|50|3:||' . I18N::translate('Age') . '|';
4114
            } else {
4115
                $chxl .= '1:||' . I18N::translate('century') . '|2:|0|10|20|30|40|50|60|70|80|90|100|3:||' . I18N::translate('Age') . '|';
4116
            }
4117
            if (count($rows) > 4 || mb_strlen(I18N::translate('Average age in century of marriage')) < 30) {
4118
                $chtt = I18N::translate('Average age in century of marriage');
4119
            } else {
4120
                $offset  = 0;
4121
                $counter = array();
4122
                while ($offset = strpos(I18N::translate('Average age in century of marriage'), ' ', $offset + 1)) {
4123
                    $counter[] = $offset;
4124
                }
4125
                $half = (int) (count($counter) / 2);
4126
                $chtt = substr_replace(I18N::translate('Average age in century of marriage'), '|', $counter[$half], 1);
4127
            }
4128
4129
            return "<img src=\"" . "https://chart.googleapis.com/chart?cht=bvg&amp;chs={$sizes[0]}x{$sizes[1]}&amp;chm=D,FF0000,2,0,3,1|{$chmm}{$chmf}&amp;chf=bg,s,ffffff00|c,s,ffffff00&amp;chtt=" . rawurlencode($chtt) . "&amp;chd={$chd}&amp;chco=0000FF,FFA0CB,FF0000&amp;chbh=20,3&amp;chxt=x,x,y,y&amp;chxl=" . rawurlencode($chxl) . "&amp;chdl=" . rawurlencode(I18N::translate('Males') . "|" . I18N::translate('Females') . "|" . I18N::translate('Average age')) . "\" width=\"{$sizes[0]}\" height=\"{$sizes[1]}\" alt=\"" . I18N::translate('Average age in century of marriage') . "\" title=\"" . I18N::translate('Average age in century of marriage') . "\" />";
0 ignored issues
show
Bug introduced by
It seems like $chtt can also be of type array; however, parameter $string of rawurlencode() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

4129
            return "<img src=\"" . "https://chart.googleapis.com/chart?cht=bvg&amp;chs={$sizes[0]}x{$sizes[1]}&amp;chm=D,FF0000,2,0,3,1|{$chmm}{$chmf}&amp;chf=bg,s,ffffff00|c,s,ffffff00&amp;chtt=" . rawurlencode(/** @scrutinizer ignore-type */ $chtt) . "&amp;chd={$chd}&amp;chco=0000FF,FFA0CB,FF0000&amp;chbh=20,3&amp;chxt=x,x,y,y&amp;chxl=" . rawurlencode($chxl) . "&amp;chdl=" . rawurlencode(I18N::translate('Males') . "|" . I18N::translate('Females') . "|" . I18N::translate('Average age')) . "\" width=\"{$sizes[0]}\" height=\"{$sizes[1]}\" alt=\"" . I18N::translate('Average age in century of marriage') . "\" title=\"" . I18N::translate('Average age in century of marriage') . "\" />";
Loading history...
4130
        } else {
4131
            if ($year1 >= 0 && $year2 >= 0) {
4132
                $years = " married.d_year BETWEEN {$year1} AND {$year2} AND ";
4133
            } else {
4134
                $years = '';
4135
            }
4136
            $rows = $this->runSql(
4137
                "SELECT " .
4138
                " fam.f_id, " .
4139
                " birth.d_gid, " .
4140
                " married.d_julianday2-birth.d_julianday1 AS age " .
4141
                "FROM `##dates` AS married " .
4142
                "JOIN `##families` AS fam ON (married.d_gid=fam.f_id AND married.d_file=fam.f_file) " .
4143
                "JOIN `##dates` AS birth ON (birth.d_gid=fam.f_husb AND birth.d_file=fam.f_file) " .
4144
                "WHERE " .
4145
                " '{$sex}' IN ('M', 'BOTH') AND {$years} " .
4146
                " married.d_file={$this->tree->getTreeId()} AND married.d_type IN ('@#DGREGORIAN@', '@#DJULIAN@') AND married.d_fact='MARR' AND " .
4147
                " birth.d_type IN ('@#DGREGORIAN@', '@#DJULIAN@') AND birth.d_fact='BIRT' AND " .
4148
                " married.d_julianday1>birth.d_julianday1 AND birth.d_julianday1<>0 " .
4149
                "UNION ALL " .
4150
                "SELECT " .
4151
                " fam.f_id, " .
4152
                " birth.d_gid, " .
4153
                " married.d_julianday2-birth.d_julianday1 AS age " .
4154
                "FROM `##dates` AS married " .
4155
                "JOIN `##families` AS fam ON (married.d_gid=fam.f_id AND married.d_file=fam.f_file) " .
4156
                "JOIN `##dates` AS birth ON (birth.d_gid=fam.f_wife AND birth.d_file=fam.f_file) " .
4157
                "WHERE " .
4158
                " '{$sex}' IN ('F', 'BOTH') AND {$years} " .
4159
                " married.d_file={$this->tree->getTreeId()} AND married.d_type IN ('@#DGREGORIAN@', '@#DJULIAN@') AND married.d_fact='MARR' AND " .
4160
                " birth.d_type IN ('@#DGREGORIAN@', '@#DJULIAN@') AND birth.d_fact='BIRT' AND " .
4161
                " married.d_julianday1>birth.d_julianday1 AND birth.d_julianday1<>0 "
4162
            );
4163
4164
            return $rows;
4165
        }
4166
    }
4167
4168
    /**
4169
     * Find the youngest wife.
4170
     *
4171
     * @return string
4172
     */
4173
    public function youngestMarriageFemale()
4174
    {
4175
        return $this->marriageQuery('full', 'ASC', 'F', false);
4176
    }
4177
4178
    /**
4179
     * Find the name of the youngest wife.
4180
     *
4181
     * @return string
4182
     */
4183
    public function youngestMarriageFemaleName()
4184
    {
4185
        return $this->marriageQuery('name', 'ASC', 'F', false);
4186
    }
4187
4188
    /**
4189
     * Find the age of the youngest wife.
4190
     *
4191
     * @param bool $show_years
4192
     *
4193
     * @return string
4194
     */
4195
    public function youngestMarriageFemaleAge($show_years = false)
4196
    {
4197
        return $this->marriageQuery('age', 'ASC', 'F', $show_years);
4198
    }
4199
4200
    /**
4201
     * Find the oldest wife.
4202
     *
4203
     * @return string
4204
     */
4205
    public function oldestMarriageFemale()
4206
    {
4207
        return $this->marriageQuery('full', 'DESC', 'F', false);
4208
    }
4209
4210
    /**
4211
     * Find the name of the oldest wife.
4212
     *
4213
     * @return string
4214
     */
4215
    public function oldestMarriageFemaleName()
4216
    {
4217
        return $this->marriageQuery('name', 'DESC', 'F', false);
4218
    }
4219
4220
    /**
4221
     * Find the age of the oldest wife.
4222
     *
4223
     * @param bool $show_years
4224
     *
4225
     * @return string
4226
     */
4227
    public function oldestMarriageFemaleAge($show_years = false)
4228
    {
4229
        return $this->marriageQuery('age', 'DESC', 'F', $show_years);
4230
    }
4231
4232
    /**
4233
     * Find the youngest husband.
4234
     *
4235
     * @return string
4236
     */
4237
    public function youngestMarriageMale()
4238
    {
4239
        return $this->marriageQuery('full', 'ASC', 'M', false);
4240
    }
4241
4242
    /**
4243
     * Find the name of the youngest husband.
4244
     *
4245
     * @return string
4246
     */
4247
    public function youngestMarriageMaleName()
4248
    {
4249
        return $this->marriageQuery('name', 'ASC', 'M', false);
4250
    }
4251
4252
    /**
4253
     * Find the age of the youngest husband.
4254
     *
4255
     * @param bool $show_years
4256
     *
4257
     * @return string
4258
     */
4259
    public function youngestMarriageMaleAge($show_years = false)
4260
    {
4261
        return $this->marriageQuery('age', 'ASC', 'M', $show_years);
4262
    }
4263
4264
    /**
4265
     * Find the oldest husband.
4266
     *
4267
     * @return string
4268
     */
4269
    public function oldestMarriageMale()
4270
    {
4271
        return $this->marriageQuery('full', 'DESC', 'M', false);
4272
    }
4273
4274
    /**
4275
     * Find the name of the oldest husband.
4276
     *
4277
     * @return string
4278
     */
4279
    public function oldestMarriageMaleName()
4280
    {
4281
        return $this->marriageQuery('name', 'DESC', 'M', false);
4282
    }
4283
4284
    /**
4285
     * Find the age of the oldest husband.
4286
     *
4287
     * @param bool $show_years
4288
     *
4289
     * @return string
4290
     */
4291
    public function oldestMarriageMaleAge($show_years = false)
4292
    {
4293
        return $this->marriageQuery('age', 'DESC', 'M', $show_years);
4294
    }
4295
4296
    /**
4297
     * General query on marriage ages.
4298
     *
4299
     * @param string[] $params
4300
     *
4301
     * @return string
4302
     */
4303
    public function statsMarrAge($params = array())
4304
    {
4305
        return $this->statsMarrAgeQuery(true, 'BOTH', -1, -1, $params);
4306
    }
4307
4308
    /**
4309
     * Find the age between husband and wife.
4310
     *
4311
     * @param string[] $params
4312
     *
4313
     * @return string
4314
     */
4315
    public function ageBetweenSpousesMF($params = array())
4316
    {
4317
        return $this->ageBetweenSpousesQuery('nolist', 'DESC', $params);
4318
    }
4319
4320
    /**
4321
     * Find the age between husband and wife.
4322
     *
4323
     * @param string[] $params
4324
     *
4325
     * @return string
4326
     */
4327
    public function ageBetweenSpousesMFList($params = array())
4328
    {
4329
        return $this->ageBetweenSpousesQuery('list', 'DESC', $params);
4330
    }
4331
4332
    /**
4333
     * Find the age between wife and husband..
4334
     *
4335
     * @param string[] $params
4336
     *
4337
     * @return string
4338
     */
4339
    public function ageBetweenSpousesFM($params = array())
4340
    {
4341
        return $this->ageBetweenSpousesQuery('nolist', 'ASC', $params);
4342
    }
4343
4344
    /**
4345
     * Find the age between wife and husband..
4346
     *
4347
     * @param string[] $params
4348
     *
4349
     * @return string
4350
     */
4351
    public function ageBetweenSpousesFMList($params = array())
4352
    {
4353
        return $this->ageBetweenSpousesQuery('list', 'ASC', $params);
4354
    }
4355
4356
    /**
4357
     * General query on marriage ages.
4358
     *
4359
     * @return string
4360
     */
4361
    public function topAgeOfMarriageFamily()
4362
    {
4363
        return $this->ageOfMarriageQuery('name', 'DESC', array('1'));
4364
    }
4365
4366
    /**
4367
     * General query on marriage ages.
4368
     *
4369
     * @return string
4370
     */
4371
    public function topAgeOfMarriage()
4372
    {
4373
        return $this->ageOfMarriageQuery('age', 'DESC', array('1'));
4374
    }
4375
4376
    /**
4377
     * General query on marriage ages.
4378
     *
4379
     * @param string[] $params
4380
     *
4381
     * @return string
4382
     */
4383
    public function topAgeOfMarriageFamilies($params = array())
4384
    {
4385
        return $this->ageOfMarriageQuery('nolist', 'DESC', $params);
4386
    }
4387
4388
    /**
4389
     * General query on marriage ages.
4390
     *
4391
     * @param string[] $params
4392
     *
4393
     * @return string
4394
     */
4395
    public function topAgeOfMarriageFamiliesList($params = array())
4396
    {
4397
        return $this->ageOfMarriageQuery('list', 'DESC', $params);
4398
    }
4399
4400
    /**
4401
     * General query on marriage ages.
4402
     *
4403
     * @return string
4404
     */
4405
    public function minAgeOfMarriageFamily()
4406
    {
4407
        return $this->ageOfMarriageQuery('name', 'ASC', array('1'));
4408
    }
4409
4410
    /**
4411
     * General query on marriage ages.
4412
     *
4413
     * @return string
4414
     */
4415
    public function minAgeOfMarriage()
4416
    {
4417
        return $this->ageOfMarriageQuery('age', 'ASC', array('1'));
4418
    }
4419
4420
    /**
4421
     * General query on marriage ages.
4422
     *
4423
     * @param string[] $params
4424
     *
4425
     * @return string
4426
     */
4427
    public function minAgeOfMarriageFamilies($params = array())
4428
    {
4429
        return $this->ageOfMarriageQuery('nolist', 'ASC', $params);
4430
    }
4431
4432
    /**
4433
     * General query on marriage ages.
4434
     *
4435
     * @param string[] $params
4436
     *
4437
     * @return string
4438
     */
4439
    public function minAgeOfMarriageFamiliesList($params = array())
4440
    {
4441
        return $this->ageOfMarriageQuery('list', 'ASC', $params);
4442
    }
4443
4444
    /**
4445
     * Find the youngest mother
4446
     *
4447
     * @return string
4448
     */
4449
    public function youngestMother()
4450
    {
4451
        return $this->parentsQuery('full', 'ASC', 'F');
4452
    }
4453
4454
    /**
4455
     * Find the name of the youngest mother.
4456
     *
4457
     * @return string
4458
     */
4459
    public function youngestMotherName()
4460
    {
4461
        return $this->parentsQuery('name', 'ASC', 'F');
4462
    }
4463
4464
    /**
4465
     * Find the age of the youngest mother.
4466
     *
4467
     * @param bool $show_years
4468
     *
4469
     * @return string
4470
     */
4471
    public function youngestMotherAge($show_years = false)
4472
    {
4473
        return $this->parentsQuery('age', 'ASC', 'F', $show_years);
4474
    }
4475
4476
    /**
4477
     * Find the oldest mother.
4478
     *
4479
     * @return string
4480
     */
4481
    public function oldestMother()
4482
    {
4483
        return $this->parentsQuery('full', 'DESC', 'F');
4484
    }
4485
4486
    /**
4487
     * Find the name of the oldest mother.
4488
     *
4489
     * @return string
4490
     */
4491
    public function oldestMotherName()
4492
    {
4493
        return $this->parentsQuery('name', 'DESC', 'F');
4494
    }
4495
4496
    /**
4497
     * Find the age of the oldest mother.
4498
     *
4499
     * @param bool $show_years
4500
     *
4501
     * @return string
4502
     */
4503
    public function oldestMotherAge($show_years = false)
4504
    {
4505
        return $this->parentsQuery('age', 'DESC', 'F', $show_years);
4506
    }
4507
4508
    /**
4509
     * Find the youngest father.
4510
     *
4511
     * @return string
4512
     */
4513
    public function youngestFather()
4514
    {
4515
        return $this->parentsQuery('full', 'ASC', 'M');
4516
    }
4517
4518
    /**
4519
     * Find the name of the youngest father.
4520
     *
4521
     * @return string
4522
     */
4523
    public function youngestFatherName()
4524
    {
4525
        return $this->parentsQuery('name', 'ASC', 'M');
4526
    }
4527
4528
    /**
4529
     * Find the age of the youngest father.
4530
     *
4531
     * @param bool $show_years
4532
     *
4533
     * @return string
4534
     */
4535
    public function youngestFatherAge($show_years = false)
4536
    {
4537
        return $this->parentsQuery('age', 'ASC', 'M', $show_years);
4538
    }
4539
4540
    /**
4541
     * Find the oldest father.
4542
     *
4543
     * @return string
4544
     */
4545
    public function oldestFather()
4546
    {
4547
        return $this->parentsQuery('full', 'DESC', 'M');
4548
    }
4549
4550
    /**
4551
     * Find the name of the oldest father.
4552
     *
4553
     * @return string
4554
     */
4555
    public function oldestFatherName()
4556
    {
4557
        return $this->parentsQuery('name', 'DESC', 'M');
4558
    }
4559
4560
    /**
4561
     * Find the age of the oldest father.
4562
     *
4563
     * @param bool $show_years
4564
     *
4565
     * @return string
4566
     */
4567
    public function oldestFatherAge($show_years = false)
4568
    {
4569
        return $this->parentsQuery('age', 'DESC', 'M', $show_years);
4570
    }
4571
4572
    /**
4573
     * Number of husbands.
4574
     *
4575
     * @return string
4576
     */
4577
    public function totalMarriedMales()
4578
    {
4579
        $n = Database::prepare("SELECT COUNT(DISTINCT f_husb) FROM `##families` WHERE f_file=? AND f_gedcom LIKE '%\\n1 MARR%'")
4580
            ->execute(array($this->tree->getTreeId()))
4581
            ->fetchOne();
4582
4583
        return I18N::number($n);
0 ignored issues
show
Bug introduced by
It seems like $n can also be of type string; however, parameter $n of Fisharebest\Webtrees\I18N::number() does only seem to accept double, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

4583
        return I18N::number(/** @scrutinizer ignore-type */ $n);
Loading history...
4584
    }
4585
4586
    /**
4587
     * Number of wives.
4588
     *
4589
     * @return string
4590
     */
4591
    public function totalMarriedFemales()
4592
    {
4593
        $n = Database::prepare("SELECT COUNT(DISTINCT f_wife) FROM `##families` WHERE f_file=? AND f_gedcom LIKE '%\\n1 MARR%'")
4594
            ->execute(array($this->tree->getTreeId()))
4595
            ->fetchOne();
4596
4597
        return I18N::number($n);
0 ignored issues
show
Bug introduced by
It seems like $n can also be of type string; however, parameter $n of Fisharebest\Webtrees\I18N::number() does only seem to accept double, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

4597
        return I18N::number(/** @scrutinizer ignore-type */ $n);
Loading history...
4598
    }
4599
4600
    /**
4601
     * General query on family.
4602
     *
4603
     * @param string $type
4604
     *
4605
     * @return string
4606
     */
4607
    private function familyQuery($type = 'full')
4608
    {
4609
        $rows = $this->runSql(
4610
            " SELECT f_numchil AS tot, f_id AS id" .
4611
            " FROM `##families`" .
4612
            " WHERE" .
4613
            " f_file={$this->tree->getTreeId()}" .
4614
            " AND f_numchil = (" .
4615
            "  SELECT max( f_numchil )" .
4616
            "  FROM `##families`" .
4617
            "  WHERE f_file ={$this->tree->getTreeId()}" .
4618
            " )" .
4619
            " LIMIT 1"
4620
        );
4621
        if (!isset($rows[0])) {
4622
            return '';
4623
        }
4624
        $row    = $rows[0];
4625
        $family = Family::getInstance($row['id'], $this->tree);
4626
        switch ($type) {
4627
            default:
4628
            case 'full':
4629
                if ($family->canShow()) {
4630
                    $result = $family->formatList('span', false, $family->getFullName());
4631
                } else {
4632
                    $result = I18N::translate('This information is private and cannot be shown.');
4633
                }
4634
                break;
4635
            case 'size':
4636
                $result = I18N::number($row['tot']);
0 ignored issues
show
Bug introduced by
$row['tot'] of type string is incompatible with the type double expected by parameter $n of Fisharebest\Webtrees\I18N::number(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

4636
                $result = I18N::number(/** @scrutinizer ignore-type */ $row['tot']);
Loading history...
4637
                break;
4638
            case 'name':
4639
                $result = "<a href=\"" . $family->getHtmlUrl() . "\">" . $family->getFullName() . '</a>';
4640
                break;
4641
        }
4642
4643
        return $result;
4644
    }
4645
4646
    /**
4647
     * General query on families.
4648
     *
4649
     * @param string   $type
4650
     * @param string[] $params
4651
     *
4652
     * @return string
4653
     */
4654
    private function topTenFamilyQuery($type = 'list', $params = array())
4655
    {
4656
        if (isset($params[0])) {
4657
            $total = (int) $params[0];
4658
        } else {
4659
            $total = 10;
4660
        }
4661
        $rows = $this->runSql(
4662
            "SELECT f_numchil AS tot, f_id AS id" .
4663
            " FROM `##families`" .
4664
            " WHERE" .
4665
            " f_file={$this->tree->getTreeId()}" .
4666
            " ORDER BY tot DESC" .
4667
            " LIMIT " . $total
4668
        );
4669
        if (!isset($rows[0])) {
4670
            return '';
4671
        }
4672
        if (count($rows) < $total) {
4673
            $total = count($rows);
4674
        }
4675
        $top10 = array();
4676
        for ($c = 0; $c < $total; $c++) {
4677
            $family = Family::getInstance($rows[$c]['id'], $this->tree);
4678
            if ($family->canShow()) {
4679
                if ($type === 'list') {
4680
                    $top10[] =
4681
                        '<li><a href="' . $family->getHtmlUrl() . '">' . $family->getFullName() . '</a> - ' .
4682
                        I18N::plural('%s child', '%s children', $rows[$c]['tot'], I18N::number($rows[$c]['tot']));
0 ignored issues
show
Bug introduced by
$rows[$c]['tot'] of type string is incompatible with the type double expected by parameter $n of Fisharebest\Webtrees\I18N::number(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

4682
                        I18N::plural('%s child', '%s children', $rows[$c]['tot'], I18N::number(/** @scrutinizer ignore-type */ $rows[$c]['tot']));
Loading history...
4683
                } else {
4684
                    $top10[] =
4685
                        '<a href="' . $family->getHtmlUrl() . '">' . $family->getFullName() . '</a> - ' .
4686
                        I18N::plural('%s child', '%s children', $rows[$c]['tot'], I18N::number($rows[$c]['tot']));
4687
                }
4688
            }
4689
        }
4690
        if ($type === 'list') {
4691
            $top10 = implode('', $top10);
4692
        } else {
4693
            $top10 = implode('; ', $top10);
4694
        }
4695
        if (I18N::direction() === 'rtl') {
4696
            $top10 = str_replace(array('[', ']', '(', ')', '+'), array('&rlm;[', '&rlm;]', '&rlm;(', '&rlm;)', '&rlm;+'), $top10);
4697
        }
4698
        if ($type === 'list') {
4699
            return '<ul>' . $top10 . '</ul>';
4700
        }
4701
4702
        return $top10;
4703
    }
4704
4705
    /**
4706
     * Find the ages between siblings.
4707
     *
4708
     * @param string   $type
4709
     * @param string[] $params
4710
     *
4711
     * @return string
4712
     */
4713
    private function ageBetweenSiblingsQuery($type = 'list', $params = array())
4714
    {
4715
        if (isset($params[0])) {
4716
            $total = (int) $params[0];
4717
        } else {
4718
            $total = 10;
4719
        }
4720
        if (isset($params[1])) {
4721
            $one = $params[1];
4722
        } else {
4723
            $one = false;
4724
        } // each family only once if true
4725
        $rows = $this->runSql(
4726
            " SELECT DISTINCT" .
4727
            " link1.l_from AS family," .
4728
            " link1.l_to AS ch1," .
4729
            " link2.l_to AS ch2," .
4730
            " child1.d_julianday2-child2.d_julianday2 AS age" .
4731
            " FROM `##link` AS link1" .
4732
            " LEFT JOIN `##dates` AS child1 ON child1.d_file = {$this->tree->getTreeId()}" .
4733
            " LEFT JOIN `##dates` AS child2 ON child2.d_file = {$this->tree->getTreeId()}" .
4734
            " LEFT JOIN `##link` AS link2 ON link2.l_file = {$this->tree->getTreeId()}" .
4735
            " WHERE" .
4736
            " link1.l_file = {$this->tree->getTreeId()} AND" .
4737
            " link1.l_from = link2.l_from AND" .
4738
            " link1.l_type = 'CHIL' AND" .
4739
            " child1.d_gid = link1.l_to AND" .
4740
            " child1.d_fact = 'BIRT' AND" .
4741
            " link2.l_type = 'CHIL' AND" .
4742
            " child2.d_gid = link2.l_to AND" .
4743
            " child2.d_fact = 'BIRT' AND" .
4744
            " child1.d_julianday2 > child2.d_julianday2 AND" .
4745
            " child2.d_julianday2 <> 0 AND" .
4746
            " child1.d_gid <> child2.d_gid" .
4747
            " ORDER BY age DESC" .
4748
            " LIMIT " . $total
4749
        );
4750
        if (!isset($rows[0])) {
4751
            return '';
4752
        }
4753
        $top10 = array();
4754
        $dist  = array();
4755
        foreach ($rows as $fam) {
4756
            $family = Family::getInstance($fam['family'], $this->tree);
4757
            $child1 = Individual::getInstance($fam['ch1'], $this->tree);
4758
            $child2 = Individual::getInstance($fam['ch2'], $this->tree);
4759
            if ($type == 'name') {
4760
                if ($child1->canShow() && $child2->canShow()) {
4761
                    $return = '<a href="' . $child2->getHtmlUrl() . '">' . $child2->getFullName() . '</a> ';
4762
                    $return .= I18N::translate('and') . ' ';
4763
                    $return .= '<a href="' . $child1->getHtmlUrl() . '">' . $child1->getFullName() . '</a>';
4764
                    $return .= ' <a href="' . $family->getHtmlUrl() . '">[' . I18N::translate('View this family') . ']</a>';
4765
                } else {
4766
                    $return = I18N::translate('This information is private and cannot be shown.');
4767
                }
4768
4769
                return $return;
4770
            }
4771
            $age = $fam['age'];
4772
            if ((int) ($age / 365.25) > 0) {
4773
                $age = (int) ($age / 365.25) . 'y';
4774
            } elseif ((int) ($age / 30.4375) > 0) {
4775
                $age = (int) ($age / 30.4375) . 'm';
4776
            } else {
4777
                $age = $age . 'd';
4778
            }
4779
            $age = FunctionsDate::getAgeAtEvent($age);
4780
            if ($type == 'age') {
4781
                return $age;
4782
            }
4783
            if ($type == 'list') {
4784
                if ($one && !in_array($fam['family'], $dist)) {
4785
                    if ($child1->canShow() && $child2->canShow()) {
4786
                        $return = "<li>";
4787
                        $return .= "<a href=\"" . $child2->getHtmlUrl() . "\">" . $child2->getFullName() . "</a> ";
4788
                        $return .= I18N::translate('and') . " ";
4789
                        $return .= "<a href=\"" . $child1->getHtmlUrl() . "\">" . $child1->getFullName() . "</a>";
4790
                        $return .= " (" . $age . ")";
4791
                        $return .= " <a href=\"" . $family->getHtmlUrl() . "\">[" . I18N::translate('View this family') . "]</a>";
4792
                        $return .= '</li>';
4793
                        $top10[] = $return;
4794
                        $dist[]  = $fam['family'];
4795
                    }
4796
                } elseif (!$one && $child1->canShow() && $child2->canShow()) {
4797
                    $return = "<li>";
4798
                    $return .= "<a href=\"" . $child2->getHtmlUrl() . "\">" . $child2->getFullName() . "</a> ";
4799
                    $return .= I18N::translate('and') . " ";
4800
                    $return .= "<a href=\"" . $child1->getHtmlUrl() . "\">" . $child1->getFullName() . "</a>";
4801
                    $return .= " (" . $age . ")";
4802
                    $return .= " <a href=\"" . $family->getHtmlUrl() . "\">[" . I18N::translate('View this family') . "]</a>";
4803
                    $return .= '</li>';
4804
                    $top10[] = $return;
4805
                }
4806
            } else {
4807
                if ($child1->canShow() && $child2->canShow()) {
4808
                    $return = $child2->formatList('span', false, $child2->getFullName());
4809
                    $return .= "<br>" . I18N::translate('and') . "<br>";
4810
                    $return .= $child1->formatList('span', false, $child1->getFullName());
4811
                    $return .= "<br><a href=\"" . $family->getHtmlUrl() . "\">[" . I18N::translate('View this family') . "]</a>";
4812
4813
                    return $return;
4814
                } else {
4815
                    return I18N::translate('This information is private and cannot be shown.');
4816
                }
4817
            }
4818
        }
4819
        if ($type === 'list') {
4820
            $top10 = implode('', $top10);
4821
        }
4822
        if (I18N::direction() === 'rtl') {
4823
            $top10 = str_replace(array('[', ']', '(', ')', '+'), array('&rlm;[', '&rlm;]', '&rlm;(', '&rlm;)', '&rlm;+'), $top10);
4824
        }
4825
        if ($type === 'list') {
4826
            return '<ul>' . $top10 . '</ul>';
4827
        }
4828
4829
        return $top10;
4830
    }
4831
4832
    /**
4833
     * Find the month in the year of the birth of the first child.
4834
     *
4835
     * @param bool     $simple
4836
     * @param bool     $sex
4837
     * @param int      $year1
4838
     * @param int      $year2
4839
     * @param string[] $params
4840
     *
4841
     * @return string|string[][]
4842
     */
4843
    public function monthFirstChildQuery($simple = true, $sex = false, $year1 = -1, $year2 = -1, $params = array())
4844
    {
4845
        $WT_STATS_CHART_COLOR1 = Theme::theme()->parameter('distribution-chart-no-values');
4846
        $WT_STATS_CHART_COLOR2 = Theme::theme()->parameter('distribution-chart-high-values');
4847
        $WT_STATS_S_CHART_X    = Theme::theme()->parameter('stats-small-chart-x');
4848
        $WT_STATS_S_CHART_Y    = Theme::theme()->parameter('stats-small-chart-y');
4849
4850
        if ($year1 >= 0 && $year2 >= 0) {
4851
            $sql_years = " AND (d_year BETWEEN '{$year1}' AND '{$year2}')";
4852
        } else {
4853
            $sql_years = '';
4854
        }
4855
        if ($sex) {
4856
            $sql_sex1 = ', i_sex';
4857
            $sql_sex2 = " JOIN `##individuals` AS child ON child1.d_file = i_file AND child1.d_gid = child.i_id ";
4858
        } else {
4859
            $sql_sex1 = '';
4860
            $sql_sex2 = '';
4861
        }
4862
        $sql =
4863
            "SELECT d_month{$sql_sex1}, COUNT(*) AS total " .
4864
            "FROM (" .
4865
            " SELECT family{$sql_sex1}, MIN(date) AS d_date, d_month" .
4866
            " FROM (" .
4867
            "  SELECT" .
4868
            "  link1.l_from AS family," .
4869
            "  link1.l_to AS child," .
4870
            "  child1.d_julianday2 AS date," .
4871
            "  child1.d_month as d_month" .
4872
            $sql_sex1 .
4873
            "  FROM `##link` AS link1" .
4874
            "  LEFT JOIN `##dates` AS child1 ON child1.d_file = {$this->tree->getTreeId()}" .
4875
            $sql_sex2 .
4876
            "  WHERE" .
4877
            "  link1.l_file = {$this->tree->getTreeId()} AND" .
4878
            "  link1.l_type = 'CHIL' AND" .
4879
            "  child1.d_gid = link1.l_to AND" .
4880
            "  child1.d_fact = 'BIRT' AND" .
4881
            "  child1.d_month IN ('JAN', 'FEB', 'MAR', 'APR', 'MAY', 'JUN', 'JUL', 'AUG', 'SEP', 'OCT', 'NOV', 'DEC')" .
4882
            $sql_years .
4883
            "  ORDER BY date" .
4884
            " ) AS children" .
4885
            " GROUP BY family, d_month{$sql_sex1}" .
4886
            ") AS first_child " .
4887
            "GROUP BY d_month";
4888
        if ($sex) {
4889
            $sql .= ', i_sex';
4890
        }
4891
        $rows = $this->runSql($sql);
4892
        if ($simple) {
4893
            if (isset($params[0]) && $params[0] != '') {
4894
                $size = strtolower($params[0]);
4895
            } else {
4896
                $size = $WT_STATS_S_CHART_X . 'x' . $WT_STATS_S_CHART_Y;
4897
            }
4898
            if (isset($params[1]) && $params[1] != '') {
4899
                $color_from = strtolower($params[1]);
4900
            } else {
4901
                $color_from = $WT_STATS_CHART_COLOR1;
4902
            }
4903
            if (isset($params[2]) && $params[2] != '') {
4904
                $color_to = strtolower($params[2]);
4905
            } else {
4906
                $color_to = $WT_STATS_CHART_COLOR2;
4907
            }
4908
            $sizes = explode('x', $size);
4909
            $tot   = 0;
4910
            foreach ($rows as $values) {
4911
                $tot += $values['total'];
4912
            }
4913
            // Beware divide by zero
4914
            if ($tot == 0) {
0 ignored issues
show
introduced by
The condition $tot == 0 is always true.
Loading history...
4915
                return '';
4916
            }
4917
            $text   = '';
4918
            $counts = array();
4919
            foreach ($rows as $values) {
4920
                $counts[] = round(100 * $values['total'] / $tot, 0);
4921
                switch ($values['d_month']) {
4922
                    default:
4923
                    case 'JAN':
4924
                        $values['d_month'] = 1;
4925
                        break;
4926
                    case 'FEB':
4927
                        $values['d_month'] = 2;
4928
                        break;
4929
                    case 'MAR':
4930
                        $values['d_month'] = 3;
4931
                        break;
4932
                    case 'APR':
4933
                        $values['d_month'] = 4;
4934
                        break;
4935
                    case 'MAY':
4936
                        $values['d_month'] = 5;
4937
                        break;
4938
                    case 'JUN':
4939
                        $values['d_month'] = 6;
4940
                        break;
4941
                    case 'JUL':
4942
                        $values['d_month'] = 7;
4943
                        break;
4944
                    case 'AUG':
4945
                        $values['d_month'] = 8;
4946
                        break;
4947
                    case 'SEP':
4948
                        $values['d_month'] = 9;
4949
                        break;
4950
                    case 'OCT':
4951
                        $values['d_month'] = 10;
4952
                        break;
4953
                    case 'NOV':
4954
                        $values['d_month'] = 11;
4955
                        break;
4956
                    case 'DEC':
4957
                        $values['d_month'] = 12;
4958
                        break;
4959
                }
4960
                $text .= I18N::translate(ucfirst(strtolower(($values['d_month'])))) . ' - ' . $values['total'] . '|';
4961
            }
4962
            $chd = $this->arrayToExtendedEncoding($counts);
4963
            $chl = substr($text, 0, -1);
4964
4965
            return '<img src="https://chart.googleapis.com/chart?cht=p3&amp;chd=e:' . $chd . '&amp;chs=' . $size . '&amp;chco=' . $color_from . ',' . $color_to . '&amp;chf=bg,s,ffffff00&amp;chl=' . $chl . '" width="' . $sizes[0] . '" height="' . $sizes[1] . '" alt="' . I18N::translate('Month of birth of first child in a relation') . '" title="' . I18N::translate('Month of birth of first child in a relation') . '" />';
4966
        }
4967
4968
        return $rows;
4969
    }
4970
4971
    /**
4972
     * Find the family with the most children.
4973
     *
4974
     * @return string
4975
     */
4976
    public function largestFamily()
4977
    {
4978
        return $this->familyQuery('full');
4979
    }
4980
4981
    /**
4982
     * Find the number of children in the largest family.
4983
     *
4984
     * @return string
4985
     */
4986
    public function largestFamilySize()
4987
    {
4988
        return $this->familyQuery('size');
4989
    }
4990
4991
    /**
4992
     * Find the family with the most children.
4993
     *
4994
     * @return string
4995
     */
4996
    public function largestFamilyName()
4997
    {
4998
        return $this->familyQuery('name');
4999
    }
5000
5001
    /**
5002
     * The the families with the most children.
5003
     *
5004
     * @param string[] $params
5005
     *
5006
     * @return string
5007
     */
5008
    public function topTenLargestFamily($params = array())
5009
    {
5010
        return $this->topTenFamilyQuery('nolist', $params);
5011
    }
5012
5013
    /**
5014
     * Find the families with the most children.
5015
     *
5016
     * @param string[] $params
5017
     *
5018
     * @return string
5019
     */
5020
    public function topTenLargestFamilyList($params = array())
5021
    {
5022
        return $this->topTenFamilyQuery('list', $params);
5023
    }
5024
5025
    /**
5026
     * Create a chart of the largest families.
5027
     *
5028
     * @param string[] $params
5029
     *
5030
     * @return string
5031
     */
5032
    public function chartLargestFamilies($params = array())
5033
    {
5034
        $WT_STATS_CHART_COLOR1 = Theme::theme()->parameter('distribution-chart-no-values');
5035
        $WT_STATS_CHART_COLOR2 = Theme::theme()->parameter('distribution-chart-high-values');
5036
        $WT_STATS_L_CHART_X    = Theme::theme()->parameter('stats-large-chart-x');
5037
        $WT_STATS_S_CHART_Y    = Theme::theme()->parameter('stats-small-chart-y');
5038
5039
        if (isset($params[0]) && $params[0] != '') {
5040
            $size = strtolower($params[0]);
5041
        } else {
5042
            $size = $WT_STATS_L_CHART_X . 'x' . $WT_STATS_S_CHART_Y;
5043
        }
5044
        if (isset($params[1]) && $params[1] != '') {
5045
            $color_from = strtolower($params[1]);
5046
        } else {
5047
            $color_from = $WT_STATS_CHART_COLOR1;
5048
        }
5049
        if (isset($params[2]) && $params[2] != '') {
5050
            $color_to = strtolower($params[2]);
5051
        } else {
5052
            $color_to = $WT_STATS_CHART_COLOR2;
5053
        }
5054
        if (isset($params[3]) && $params[3] != '') {
5055
            $total = strtolower($params[3]);
5056
        } else {
5057
            $total = 10;
5058
        }
5059
        $sizes = explode('x', $size);
5060
        $total = (int) $total;
5061
        $rows  = $this->runSql(
5062
            " SELECT f_numchil AS tot, f_id AS id" .
5063
            " FROM `##families`" .
5064
            " WHERE f_file={$this->tree->getTreeId()}" .
5065
            " ORDER BY tot DESC" .
5066
            " LIMIT " . $total
5067
        );
5068
        if (!isset($rows[0])) {
5069
            return '';
5070
        }
5071
        $tot = 0;
5072
        foreach ($rows as $row) {
5073
            $tot += (int) $row['tot'];
5074
        }
5075
        $chd = '';
5076
        $chl = array();
5077
        foreach ($rows as $row) {
5078
            $family = Family::getInstance($row['id'], $this->tree);
5079
            if ($family->canShow()) {
5080
                if ($tot == 0) {
5081
                    $per = 0;
5082
                } else {
5083
                    $per = round(100 * $row['tot'] / $tot, 0);
5084
                }
5085
                $chd .= $this->arrayToExtendedEncoding(array($per));
0 ignored issues
show
Bug introduced by
array($per) of type array<integer,double|integer> is incompatible with the type integer[] expected by parameter $a of Fisharebest\Webtrees\Sta...rayToExtendedEncoding(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

5085
                $chd .= $this->arrayToExtendedEncoding(/** @scrutinizer ignore-type */ array($per));
Loading history...
5086
                $chl[] = htmlspecialchars_decode(strip_tags($family->getFullName())) . ' - ' . I18N::number($row['tot']);
0 ignored issues
show
Bug introduced by
$row['tot'] of type string is incompatible with the type double expected by parameter $n of Fisharebest\Webtrees\I18N::number(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

5086
                $chl[] = htmlspecialchars_decode(strip_tags($family->getFullName())) . ' - ' . I18N::number(/** @scrutinizer ignore-type */ $row['tot']);
Loading history...
5087
            }
5088
        }
5089
        $chl = rawurlencode(implode('|', $chl));
5090
5091
        return "<img src=\"https://chart.googleapis.com/chart?cht=p3&amp;chd=e:{$chd}&amp;chs={$size}&amp;chco={$color_from},{$color_to}&amp;chf=bg,s,ffffff00&amp;chl={$chl}\" width=\"{$sizes[0]}\" height=\"{$sizes[1]}\" alt=\"" . I18N::translate('Largest families') . "\" title=\"" . I18N::translate('Largest families') . "\" />";
5092
    }
5093
5094
    /**
5095
     * Count the total children.
5096
     *
5097
     * @return string
5098
     */
5099
    public function totalChildren()
5100
    {
5101
        $rows = $this->runSql("SELECT SUM(f_numchil) AS tot FROM `##families` WHERE f_file={$this->tree->getTreeId()}");
5102
5103
        return I18N::number($rows[0]['tot']);
0 ignored issues
show
Bug introduced by
$rows[0]['tot'] of type string is incompatible with the type double expected by parameter $n of Fisharebest\Webtrees\I18N::number(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

5103
        return I18N::number(/** @scrutinizer ignore-type */ $rows[0]['tot']);
Loading history...
5104
    }
5105
5106
    /**
5107
     * Find the average number of children in families.
5108
     *
5109
     * @return string
5110
     */
5111
    public function averageChildren()
5112
    {
5113
        $rows = $this->runSql("SELECT AVG(f_numchil) AS tot FROM `##families` WHERE f_file={$this->tree->getTreeId()}");
5114
5115
        return I18N::number($rows[0]['tot'], 2);
0 ignored issues
show
Bug introduced by
$rows[0]['tot'] of type string is incompatible with the type double expected by parameter $n of Fisharebest\Webtrees\I18N::number(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

5115
        return I18N::number(/** @scrutinizer ignore-type */ $rows[0]['tot'], 2);
Loading history...
5116
    }
5117
5118
    /**
5119
     * General query on familes/children.
5120
     *
5121
     * @param bool     $simple
5122
     * @param string   $sex
5123
     * @param int      $year1
5124
     * @param int      $year2
5125
     * @param string[] $params
5126
     *
5127
     * @return string|string[][]
5128
     */
5129
    public function statsChildrenQuery($simple = true, $sex = 'BOTH', $year1 = -1, $year2 = -1, $params = array())
5130
    {
5131
        if ($simple) {
5132
            if (isset($params[0]) && $params[0] != '') {
5133
                $size = strtolower($params[0]);
5134
            } else {
5135
                $size = '220x200';
5136
            }
5137
            $sizes = explode('x', $size);
5138
            $max   = 0;
5139
            $rows  = $this->runSql(
5140
                " SELECT ROUND(AVG(f_numchil),2) AS num, FLOOR(d_year/100+1) AS century" .
5141
                " FROM  `##families`" .
5142
                " JOIN  `##dates` ON (d_file = f_file AND d_gid=f_id)" .
5143
                " WHERE f_file = {$this->tree->getTreeId()}" .
5144
                " AND   d_julianday1<>0" .
5145
                " AND   d_fact = 'MARR'" .
5146
                " AND   d_type IN ('@#DGREGORIAN@', '@#DJULIAN@')" .
5147
                " GROUP BY century" .
5148
                " ORDER BY century");
5149
            if (empty($rows)) {
5150
                return '';
5151
            }
5152
            foreach ($rows as $values) {
5153
                if ($max < $values['num']) {
5154
                    $max = $values['num'];
5155
                }
5156
            }
5157
            $chm    = "";
5158
            $chxl   = "0:|";
5159
            $i      = 0;
5160
            $counts = array();
5161
            foreach ($rows as $values) {
5162
                if ($sizes[0] < 980) {
5163
                    $sizes[0] += 38;
5164
                }
5165
                $chxl .= $this->centuryName($values['century']) . "|";
0 ignored issues
show
Bug introduced by
$values['century'] of type string is incompatible with the type integer expected by parameter $century of Fisharebest\Webtrees\Stats::centuryName(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

5165
                $chxl .= $this->centuryName(/** @scrutinizer ignore-type */ $values['century']) . "|";
Loading history...
5166
                if ($max <= 5) {
5167
                    $counts[] = round($values['num'] * 819.2 - 1, 1);
5168
                } elseif ($max <= 10) {
5169
                    $counts[] = round($values['num'] * 409.6, 1);
5170
                } else {
5171
                    $counts[] = round($values['num'] * 204.8, 1);
5172
                }
5173
                $chm .= 't' . $values['num'] . ',000000,0,' . $i . ',11,1|';
5174
                $i++;
5175
            }
5176
            $chd = $this->arrayToExtendedEncoding($counts);
0 ignored issues
show
Bug introduced by
It seems like $counts can also be of type array<mixed,double>; however, parameter $a of Fisharebest\Webtrees\Sta...rayToExtendedEncoding() does only seem to accept integer[], maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

5176
            $chd = $this->arrayToExtendedEncoding(/** @scrutinizer ignore-type */ $counts);
Loading history...
5177
            $chm = substr($chm, 0, -1);
5178
            if ($max <= 5) {
5179
                $chxl .= "1:||" . I18N::translate('century') . "|2:|0|1|2|3|4|5|3:||" . I18N::translate('Number of children') . "|";
5180
            } elseif ($max <= 10) {
5181
                $chxl .= "1:||" . I18N::translate('century') . "|2:|0|1|2|3|4|5|6|7|8|9|10|3:||" . I18N::translate('Number of children') . "|";
5182
            } else {
5183
                $chxl .= "1:||" . I18N::translate('century') . "|2:|0|1|2|3|4|5|6|7|8|9|10|11|12|13|14|15|16|17|18|19|20|3:||" . I18N::translate('Number of children') . "|";
5184
            }
5185
5186
            return "<img src=\"https://chart.googleapis.com/chart?cht=bvg&amp;chs={$sizes[0]}x{$sizes[1]}&amp;chf=bg,s,ffffff00|c,s,ffffff00&amp;chm=D,FF0000,0,0,3,1|{$chm}&amp;chd=e:{$chd}&amp;chco=0000FF&amp;chbh=30,3&amp;chxt=x,x,y,y&amp;chxl=" . rawurlencode($chxl) . "\" width=\"{$sizes[0]}\" height=\"{$sizes[1]}\" alt=\"" . I18N::translate('Average number of children per family') . "\" title=\"" . I18N::translate('Average number of children per family') . "\" />";
5187
        } else {
5188
            if ($sex == 'M') {
5189
                $sql =
5190
                    "SELECT num, COUNT(*) AS total FROM " .
5191
                    "(SELECT count(i_sex) AS num FROM `##link` " .
5192
                    "LEFT OUTER JOIN `##individuals` " .
5193
                    "ON l_from=i_id AND l_file=i_file AND i_sex='M' AND l_type='FAMC' " .
5194
                    "JOIN `##families` ON f_file=l_file AND f_id=l_to WHERE f_file={$this->tree->getTreeId()} GROUP BY l_to" .
5195
                    ") boys" .
5196
                    " GROUP BY num" .
5197
                    " ORDER BY num";
5198
            } elseif ($sex == 'F') {
5199
                $sql =
5200
                    "SELECT num, COUNT(*) AS total FROM " .
5201
                    "(SELECT count(i_sex) AS num FROM `##link` " .
5202
                    "LEFT OUTER JOIN `##individuals` " .
5203
                    "ON l_from=i_id AND l_file=i_file AND i_sex='F' AND l_type='FAMC' " .
5204
                    "JOIN `##families` ON f_file=l_file AND f_id=l_to WHERE f_file={$this->tree->getTreeId()} GROUP BY l_to" .
5205
                    ") girls" .
5206
                    " GROUP BY num" .
5207
                    " ORDER BY num";
5208
            } else {
5209
                $sql = "SELECT f_numchil, COUNT(*) AS total FROM `##families` ";
5210
                if ($year1 >= 0 && $year2 >= 0) {
5211
                    $sql .=
5212
                        "AS fam LEFT JOIN `##dates` AS married ON married.d_file = {$this->tree->getTreeId()}"
5213
                        . " WHERE"
5214
                        . " married.d_gid = fam.f_id AND"
5215
                        . " fam.f_file = {$this->tree->getTreeId()} AND"
5216
                        . " married.d_fact = 'MARR' AND"
5217
                        . " married.d_year BETWEEN '{$year1}' AND '{$year2}'";
5218
                } else {
5219
                    $sql .= "WHERE f_file={$this->tree->getTreeId()}";
5220
                }
5221
                $sql .= " GROUP BY f_numchil";
5222
            }
5223
            $rows = $this->runSql($sql);
5224
5225
            return $rows;
5226
        }
5227
    }
5228
5229
    /**
5230
     * Genearl query on families/children.
5231
     *
5232
     * @param string[] $params
5233
     *
5234
     * @return string
5235
     */
5236
    public function statsChildren($params = array())
5237
    {
5238
        return $this->statsChildrenQuery(true, 'BOTH', -1, -1, $params);
5239
    }
5240
5241
    /**
5242
     * Find the names of siblings with the widest age gap.
5243
     *
5244
     * @param string[] $params
5245
     *
5246
     * @return string
5247
     */
5248
    public function topAgeBetweenSiblingsName($params = array())
5249
    {
5250
        return $this->ageBetweenSiblingsQuery('name', $params);
5251
    }
5252
5253
    /**
5254
     * Find the widest age gap between siblings.
5255
     *
5256
     * @param string[] $params
5257
     *
5258
     * @return string
5259
     */
5260
    public function topAgeBetweenSiblings($params = array())
5261
    {
5262
        return $this->ageBetweenSiblingsQuery('age', $params);
5263
    }
5264
5265
    /**
5266
     * Find the name of siblings with the widest age gap.
5267
     *
5268
     * @param string[] $params
5269
     *
5270
     * @return string
5271
     */
5272
    public function topAgeBetweenSiblingsFullName($params = array())
5273
    {
5274
        return $this->ageBetweenSiblingsQuery('nolist', $params);
5275
    }
5276
5277
    /**
5278
     * Find the siblings with the widest age gaps.
5279
     *
5280
     * @param string[] $params
5281
     *
5282
     * @return string
5283
     */
5284
    public function topAgeBetweenSiblingsList($params = array())
5285
    {
5286
        return $this->ageBetweenSiblingsQuery('list', $params);
5287
    }
5288
5289
    /**
5290
     * Find the families with no children.
5291
     *
5292
     * @return string
5293
     */
5294
    private function noChildrenFamiliesQuery()
5295
    {
5296
        $rows = $this->runSql(
5297
            " SELECT COUNT(*) AS tot" .
5298
            " FROM  `##families`" .
5299
            " WHERE f_numchil = 0 AND f_file = {$this->tree->getTreeId()}");
5300
5301
        return $rows[0]['tot'];
5302
    }
5303
5304
    /**
5305
     * Find the families with no children.
5306
     *
5307
     * @return string
5308
     */
5309
    public function noChildrenFamilies()
5310
    {
5311
        return I18N::number($this->noChildrenFamiliesQuery());
0 ignored issues
show
Bug introduced by
$this->noChildrenFamiliesQuery() of type string is incompatible with the type double expected by parameter $n of Fisharebest\Webtrees\I18N::number(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

5311
        return I18N::number(/** @scrutinizer ignore-type */ $this->noChildrenFamiliesQuery());
Loading history...
5312
    }
5313
5314
    /**
5315
     * Find the families with no children.
5316
     *
5317
     * @param string[] $params
5318
     *
5319
     * @return string
5320
     */
5321
    public function noChildrenFamiliesList($params = array())
5322
    {
5323
        if (isset($params[0]) && $params[0] != '') {
5324
            $type = strtolower($params[0]);
5325
        } else {
5326
            $type = 'list';
5327
        }
5328
        $rows = $this->runSql(
5329
            " SELECT f_id AS family" .
5330
            " FROM `##families` AS fam" .
5331
            " WHERE f_numchil = 0 AND fam.f_file = {$this->tree->getTreeId()}");
5332
        if (!isset($rows[0])) {
5333
            return '';
5334
        }
5335
        $top10 = array();
5336
        foreach ($rows as $row) {
5337
            $family = Family::getInstance($row['family'], $this->tree);
5338
            if ($family->canShow()) {
5339
                if ($type == 'list') {
5340
                    $top10[] = "<li><a href=\"" . $family->getHtmlUrl() . "\">" . $family->getFullName() . "</a></li>";
5341
                } else {
5342
                    $top10[] = "<a href=\"" . $family->getHtmlUrl() . "\">" . $family->getFullName() . "</a>";
5343
                }
5344
            }
5345
        }
5346
        if ($type == 'list') {
5347
            $top10 = implode('', $top10);
5348
        } else {
5349
            $top10 = implode('; ', $top10);
5350
        }
5351
        if (I18N::direction() === 'rtl') {
5352
            $top10 = str_replace(array('[', ']', '(', ')', '+'), array('&rlm;[', '&rlm;]', '&rlm;(', '&rlm;)', '&rlm;+'), $top10);
5353
        }
5354
        if ($type === 'list') {
5355
            return '<ul>' . $top10 . '</ul>';
5356
        }
5357
5358
        return $top10;
5359
    }
5360
5361
    /**
5362
     * Create a chart of children with no families.
5363
     *
5364
     * @param string[] $params
5365
     *
5366
     * @return string
5367
     */
5368
    public function chartNoChildrenFamilies($params = array())
5369
    {
5370
        if (isset($params[0]) && $params[0] != '') {
5371
            $size = strtolower($params[0]);
5372
        } else {
5373
            $size = '220x200';
5374
        }
5375
        if (isset($params[1]) && $params[1] != '') {
5376
            $year1 = $params[1];
5377
        } else {
5378
            $year1 = -1;
5379
        }
5380
        if (isset($params[2]) && $params[2] != '') {
5381
            $year2 = $params[2];
5382
        } else {
5383
            $year2 = -1;
5384
        }
5385
        $sizes = explode('x', $size);
5386
        if ($year1 >= 0 && $year2 >= 0) {
5387
            $years = " married.d_year BETWEEN '{$year1}' AND '{$year2}' AND";
5388
        } else {
5389
            $years = "";
5390
        }
5391
        $max  = 0;
5392
        $tot  = 0;
5393
        $rows = $this->runSql(
5394
            "SELECT" .
5395
            " COUNT(*) AS count," .
5396
            " FLOOR(married.d_year/100+1) AS century" .
5397
            " FROM" .
5398
            " `##families` AS fam" .
5399
            " JOIN" .
5400
            " `##dates` AS married ON (married.d_file = fam.f_file AND married.d_gid = fam.f_id)" .
5401
            " WHERE" .
5402
            " f_numchil = 0 AND" .
5403
            " fam.f_file = {$this->tree->getTreeId()} AND" .
5404
            $years .
5405
            " married.d_fact = 'MARR' AND" .
5406
            " married.d_type IN ('@#DGREGORIAN@', '@#DJULIAN@')" .
5407
            " GROUP BY century ORDER BY century"
5408
        );
5409
        if (empty($rows)) {
5410
            return '';
5411
        }
5412
        foreach ($rows as $values) {
5413
            if ($max < $values['count']) {
5414
                $max = $values['count'];
5415
            }
5416
            $tot += (int) $values['count'];
5417
        }
5418
        $unknown = $this->noChildrenFamiliesQuery() - $tot;
5419
        if ($unknown > $max) {
5420
            $max = $unknown;
5421
        }
5422
        $chm    = "";
5423
        $chxl   = "0:|";
5424
        $i      = 0;
5425
        $counts = array();
5426
        foreach ($rows as $values) {
5427
            if ($sizes[0] < 980) {
5428
                $sizes[0] += 38;
5429
            }
5430
            $chxl .= $this->centuryName($values['century']) . "|";
0 ignored issues
show
Bug introduced by
$values['century'] of type string is incompatible with the type integer expected by parameter $century of Fisharebest\Webtrees\Stats::centuryName(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

5430
            $chxl .= $this->centuryName(/** @scrutinizer ignore-type */ $values['century']) . "|";
Loading history...
5431
            $counts[] = round(4095 * $values['count'] / ($max + 1));
5432
            $chm .= 't' . $values['count'] . ',000000,0,' . $i . ',11,1|';
5433
            $i++;
5434
        }
5435
        $counts[] = round(4095 * $unknown / ($max + 1));
5436
        $chd      = $this->arrayToExtendedEncoding($counts);
0 ignored issues
show
Bug introduced by
It seems like $counts can also be of type array<mixed,double>; however, parameter $a of Fisharebest\Webtrees\Sta...rayToExtendedEncoding() does only seem to accept integer[], maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

5436
        $chd      = $this->arrayToExtendedEncoding(/** @scrutinizer ignore-type */ $counts);
Loading history...
5437
        $chm .= 't' . $unknown . ',000000,0,' . $i . ',11,1';
5438
        $chxl .= I18N::translateContext('unknown century', 'Unknown') . "|1:||" . I18N::translate('century') . "|2:|0|";
5439
        $step = $max + 1;
5440
        for ($d = (int) ($max + 1); $d > 0; $d--) {
5441
            if (($max + 1) < ($d * 10 + 1) && fmod(($max + 1), $d) == 0) {
5442
                $step = $d;
5443
            }
5444
        }
5445
        if ($step == (int) ($max + 1)) {
5446
            for ($d = (int) ($max); $d > 0; $d--) {
5447
                if ($max < ($d * 10 + 1) && fmod($max, $d) == 0) {
0 ignored issues
show
Bug introduced by
It seems like $max can also be of type string; however, parameter $num1 of fmod() does only seem to accept double, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

5447
                if ($max < ($d * 10 + 1) && fmod(/** @scrutinizer ignore-type */ $max, $d) == 0) {
Loading history...
5448
                    $step = $d;
5449
                }
5450
            }
5451
        }
5452
        for ($n = $step; $n <= ($max + 1); $n += $step) {
5453
            $chxl .= $n . "|";
5454
        }
5455
        $chxl .= "3:||" . I18N::translate('Total families') . "|";
5456
5457
        return "<img src=\"https://chart.googleapis.com/chart?cht=bvg&amp;chs={$sizes[0]}x{$sizes[1]}&amp;chf=bg,s,ffffff00|c,s,ffffff00&amp;chm=D,FF0000,0,0:" . ($i - 1) . ",3,1|{$chm}&amp;chd=e:{$chd}&amp;chco=0000FF,ffffff00&amp;chbh=30,3&amp;chxt=x,x,y,y&amp;chxl=" . rawurlencode($chxl) . "\" width=\"{$sizes[0]}\" height=\"{$sizes[1]}\" alt=\"" . I18N::translate('Number of families without children') . "\" title=\"" . I18N::translate('Number of families without children') . "\" />";
5458
    }
5459
5460
    /**
5461
     * Find the couple with the most grandchildren.
5462
     *
5463
     * @param string   $type
5464
     * @param string[] $params
5465
     *
5466
     * @return string
5467
     */
5468
    private function topTenGrandFamilyQuery($type = 'list', $params = array())
5469
    {
5470
        if (isset($params[0])) {
5471
            $total = (int) $params[0];
5472
        } else {
5473
            $total = 10;
5474
        }
5475
        $rows = $this->runSql(
5476
            "SELECT COUNT(*) AS tot, f_id AS id" .
5477
            " FROM `##families`" .
5478
            " JOIN `##link` AS children ON children.l_file = {$this->tree->getTreeId()}" .
5479
            " JOIN `##link` AS mchildren ON mchildren.l_file = {$this->tree->getTreeId()}" .
5480
            " JOIN `##link` AS gchildren ON gchildren.l_file = {$this->tree->getTreeId()}" .
5481
            " WHERE" .
5482
            " f_file={$this->tree->getTreeId()} AND" .
5483
            " children.l_from=f_id AND" .
5484
            " children.l_type='CHIL' AND" .
5485
            " children.l_to=mchildren.l_from AND" .
5486
            " mchildren.l_type='FAMS' AND" .
5487
            " mchildren.l_to=gchildren.l_from AND" .
5488
            " gchildren.l_type='CHIL'" .
5489
            " GROUP BY id" .
5490
            " ORDER BY tot DESC" .
5491
            " LIMIT " . $total
5492
        );
5493
        if (!isset($rows[0])) {
5494
            return '';
5495
        }
5496
        $top10 = array();
5497
        foreach ($rows as $row) {
5498
            $family = Family::getInstance($row['id'], $this->tree);
5499
            if ($family->canShow()) {
5500
                if ($type === 'list') {
5501
                    $top10[] =
5502
                        '<li><a href="' . $family->getHtmlUrl() . '">' . $family->getFullName() . '</a> - ' .
5503
                        I18N::plural('%s grandchild', '%s grandchildren', $row['tot'], I18N::number($row['tot']));
0 ignored issues
show
Bug introduced by
$row['tot'] of type string is incompatible with the type double expected by parameter $n of Fisharebest\Webtrees\I18N::number(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

5503
                        I18N::plural('%s grandchild', '%s grandchildren', $row['tot'], I18N::number(/** @scrutinizer ignore-type */ $row['tot']));
Loading history...
5504
                } else {
5505
                    $top10[] =
5506
                        '<a href="' . $family->getHtmlUrl() . '">' . $family->getFullName() . '</a> - ' .
5507
                        I18N::plural('%s grandchild', '%s grandchildren', $row['tot'], I18N::number($row['tot']));
5508
                }
5509
            }
5510
        }
5511
        if ($type === 'list') {
5512
            $top10 = implode('', $top10);
5513
        } else {
5514
            $top10 = implode('; ', $top10);
5515
        }
5516
        if (I18N::direction() === 'rtl') {
5517
            $top10 = str_replace(array('[', ']', '(', ')', '+'), array('&rlm;[', '&rlm;]', '&rlm;(', '&rlm;)', '&rlm;+'), $top10);
5518
        }
5519
        if ($type === 'list') {
5520
            return '<ul>' . $top10 . '</ul>';
5521
        }
5522
5523
        return $top10;
5524
    }
5525
5526
    /**
5527
     * Find the couple with the most grandchildren.
5528
     *
5529
     * @param string[] $params
5530
     *
5531
     * @return string
5532
     */
5533
    public function topTenLargestGrandFamily($params = array())
5534
    {
5535
        return $this->topTenGrandFamilyQuery('nolist', $params);
5536
    }
5537
5538
    /**
5539
     * Find the couple with the most grandchildren.
5540
     *
5541
     * @param string[] $params
5542
     *
5543
     * @return string
5544
     */
5545
    public function topTenLargestGrandFamilyList($params = array())
5546
    {
5547
        return $this->topTenGrandFamilyQuery('list', $params);
5548
    }
5549
5550
    /**
5551
     * Find common surnames.
5552
     *
5553
     * @param string   $type
5554
     * @param bool     $show_tot
5555
     * @param string[] $params
5556
     *
5557
     * @return string
5558
     */
5559
    private function commonSurnamesQuery($type = 'list', $show_tot = false, $params = array())
5560
    {
5561
        $threshold          = empty($params[0]) ? 10 : (int) $params[0];
5562
        $number_of_surnames = empty($params[1]) ? 10 : (int) $params[1];
5563
        $sorting            = empty($params[2]) ? 'alpha' : $params[2];
5564
5565
        $surname_list = FunctionsDb::getTopSurnames($this->tree->getTreeId(), $threshold, $number_of_surnames);
5566
        if (empty($surname_list)) {
5567
            return '';
5568
        }
5569
5570
        switch ($sorting) {
5571
            default:
5572
            case 'alpha':
5573
                uksort($surname_list, '\Fisharebest\Webtrees\I18N::strcasecmp');
5574
                break;
5575
            case 'count':
5576
                asort($surname_list);
5577
                break;
5578
            case 'rcount':
5579
                arsort($surname_list);
5580
                break;
5581
        }
5582
5583
        // Note that we count/display SPFX SURN, but sort/group under just SURN
5584
        $surnames = array();
5585
        foreach (array_keys($surname_list) as $surname) {
5586
            $surnames = array_merge($surnames, QueryName::surnames($this->tree, $surname, '', false, false));
5587
        }
5588
5589
        return FunctionsPrintLists::surnameList($surnames, ($type == 'list' ? 1 : 2), $show_tot, 'indilist.php', $this->tree);
5590
    }
5591
5592
    /**
5593
     * Find common surnames.
5594
     *
5595
     * @return string
5596
     */
5597
    public function getCommonSurname()
5598
    {
5599
        $surnames = array_keys(FunctionsDb::getTopSurnames($this->tree->getTreeId(), 1, 1));
5600
5601
        return array_shift($surnames);
5602
    }
5603
5604
    /**
5605
     * Find common surnames.
5606
     *
5607
     * @param string[] $params
5608
     *
5609
     * @return string
5610
     */
5611
    public function commonSurnames($params = array('', '', 'alpha'))
5612
    {
5613
        return $this->commonSurnamesQuery('nolist', false, $params);
5614
    }
5615
5616
    /**
5617
     * Find common surnames.
5618
     *
5619
     * @param string[] $params
5620
     *
5621
     * @return string
5622
     */
5623
    public function commonSurnamesTotals($params = array('', '', 'rcount'))
5624
    {
5625
        return $this->commonSurnamesQuery('nolist', true, $params);
5626
    }
5627
5628
    /**
5629
     * Find common surnames.
5630
     *
5631
     * @param string[] $params
5632
     *
5633
     * @return string
5634
     */
5635
    public function commonSurnamesList($params = array('', '', 'alpha'))
5636
    {
5637
        return $this->commonSurnamesQuery('list', false, $params);
5638
    }
5639
5640
    /**
5641
     * Find common surnames.
5642
     *
5643
     * @param string[] $params
5644
     *
5645
     * @return string
5646
     */
5647
    public function commonSurnamesListTotals($params = array('', '', 'rcount'))
5648
    {
5649
        return $this->commonSurnamesQuery('list', true, $params);
5650
    }
5651
5652
    /**
5653
     * Create a chart of common surnames.
5654
     *
5655
     * @param string[] $params
5656
     *
5657
     * @return string
5658
     */
5659
    public function chartCommonSurnames($params = array())
5660
    {
5661
        $WT_STATS_CHART_COLOR1 = Theme::theme()->parameter('distribution-chart-no-values');
5662
        $WT_STATS_CHART_COLOR2 = Theme::theme()->parameter('distribution-chart-high-values');
5663
        $WT_STATS_S_CHART_X    = Theme::theme()->parameter('stats-small-chart-x');
5664
        $WT_STATS_S_CHART_Y    = Theme::theme()->parameter('stats-small-chart-y');
5665
5666
        $size               = empty($params[0]) ? $WT_STATS_S_CHART_X . "x" . $WT_STATS_S_CHART_Y : strtolower($params[0]);
5667
        $color_from         = empty($params[1]) ? $WT_STATS_CHART_COLOR1 : strtolower($params[1]);
5668
        $color_to           = empty($params[2]) ? $WT_STATS_CHART_COLOR2 : strtolower($params[2]);
5669
        $number_of_surnames = empty($params[3]) ? 10 : (int) $params[3];
5670
5671
        $sizes    = explode('x', $size);
5672
        $tot_indi = $this->totalIndividualsQuery();
5673
        $surnames = FunctionsDb::getTopSurnames($this->tree->getTreeId(), 0, $number_of_surnames);
5674
        if (empty($surnames)) {
5675
            return '';
5676
        }
5677
        $SURNAME_TRADITION = $this->tree->getPreference('SURNAME_TRADITION');
5678
        $all_surnames      = array();
5679
        $tot               = 0;
5680
        foreach ($surnames as $surname => $num) {
5681
            $all_surnames = array_merge($all_surnames, QueryName::surnames($this->tree, I18N::strtoupper($surname), '', false, false));
5682
            $tot += $num;
5683
        }
5684
        $chd = '';
5685
        $chl = array();
5686
        foreach ($all_surnames as $surns) {
5687
            $count_per = 0;
5688
            $max_name  = 0;
5689
            $top_name  = '';
5690
            foreach ($surns as $spfxsurn => $indis) {
5691
                $per = count($indis);
5692
                $count_per += $per;
5693
                // select most common surname from all variants
5694
                if ($per > $max_name) {
5695
                    $max_name = $per;
5696
                    $top_name = $spfxsurn;
5697
                }
5698
            }
5699
            switch ($SURNAME_TRADITION) {
5700
                case 'polish':
5701
                    // most common surname should be in male variant (Kowalski, not Kowalska)
5702
                    $top_name = preg_replace(array('/ska$/', '/cka$/', '/dzka$/', '/żka$/'), array('ski', 'cki', 'dzki', 'żki'), $top_name);
5703
            }
5704
            $per = round(100 * $count_per / $tot_indi, 0);
5705
            $chd .= $this->arrayToExtendedEncoding(array($per));
0 ignored issues
show
Bug introduced by
array($per) of type array<integer,double> is incompatible with the type integer[] expected by parameter $a of Fisharebest\Webtrees\Sta...rayToExtendedEncoding(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

5705
            $chd .= $this->arrayToExtendedEncoding(/** @scrutinizer ignore-type */ array($per));
Loading history...
5706
            $chl[] = $top_name . ' - ' . I18N::number($count_per);
5707
5708
        }
5709
        $per = round(100 * ($tot_indi - $tot) / $tot_indi, 0);
5710
        $chd .= $this->arrayToExtendedEncoding(array($per));
5711
        $chl[] = I18N::translate('Other') . ' - ' . I18N::number($tot_indi - $tot);
5712
5713
        $chart_title = implode(I18N::$list_separator, $chl);
5714
        $chl         = implode('|', $chl);
5715
5716
        return '<img src="https://chart.googleapis.com/chart?cht=p3&amp;chd=e:' . $chd . '&amp;chs=' . $size . '&amp;chco=' . $color_from . ',' . $color_to . '&amp;chf=bg,s,ffffff00&amp;chl=' . rawurlencode($chl) . '" width="' . $sizes[0] . '" height="' . $sizes[1] . '" alt="' . $chart_title . '" title="' . $chart_title . '" />';
5717
    }
5718
5719
    /**
5720
     * Find common given names.
5721
     *
5722
     * @param string   $sex
5723
     * @param string   $type
5724
     * @param bool     $show_tot
5725
     * @param string[] $params
5726
     *
5727
     * @return string
5728
     */
5729
    private function commonGivenQuery($sex = 'B', $type = 'list', $show_tot = false, $params = array())
5730
    {
5731
        if (isset($params[0]) && $params[0] != '' && $params[0] >= 0) {
5732
            $threshold = (int) $params[0];
5733
        } else {
5734
            $threshold = 1;
5735
        }
5736
        if (isset($params[1]) && $params[1] != '' && $params[1] >= 0) {
5737
            $maxtoshow = (int) $params[1];
5738
        } else {
5739
            $maxtoshow = 10;
5740
        }
5741
5742
        switch ($sex) {
5743
            case 'M':
5744
                $sex_sql = "i_sex='M'";
5745
                break;
5746
            case 'F':
5747
                $sex_sql = "i_sex='F'";
5748
                break;
5749
            case 'U':
5750
                $sex_sql = "i_sex='U'";
5751
                break;
5752
            case 'B':
5753
            default:
5754
                $sex_sql = "i_sex<>'U'";
5755
                break;
5756
        }
5757
        $ged_id = $this->tree->getTreeId();
5758
5759
        $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")
5760
            ->fetchAll();
5761
        $nameList = array();
5762
        foreach ($rows as $row) {
5763
            // Split “John Thomas” into “John” and “Thomas” and count against both totals
5764
            foreach (explode(' ', $row->n_givn) as $given) {
5765
                // Exclude initials and particles.
5766
                if (!preg_match('/^([A-Z]|[a-z]{1,3})$/', $given)) {
5767
                    if (array_key_exists($given, $nameList)) {
5768
                        $nameList[$given] += $row->num;
5769
                    } else {
5770
                        $nameList[$given] = $row->num;
5771
                    }
5772
                }
5773
            }
5774
        }
5775
        arsort($nameList, SORT_NUMERIC);
5776
        $nameList = array_slice($nameList, 0, $maxtoshow);
5777
5778
        if (count($nameList) == 0) {
5779
            return '';
5780
        }
5781
        if ($type == 'chart') {
5782
            return $nameList;
5783
        }
5784
        $common = array();
5785
        foreach ($nameList as $given => $total) {
5786
            if ($maxtoshow !== -1) {
5787
                if ($maxtoshow-- <= 0) {
5788
                    break;
5789
                }
5790
            }
5791
            if ($total < $threshold) {
5792
                break;
5793
            }
5794
            if ($show_tot) {
5795
                $tot = ' (' . I18N::number($total) . ')';
5796
            } else {
5797
                $tot = '';
5798
            }
5799
            switch ($type) {
5800
                case 'table':
5801
                    $common[] = '<tr><td>' . $given . '</td><td>' . I18N::number($total) . '</td><td>' . $total . '</td></tr>';
5802
                    break;
5803
                case 'list':
5804
                    $common[] = '<li><span dir="auto">' . $given . '</span>' . $tot . '</li>';
5805
                    break;
5806
                case 'nolist':
5807
                    $common[] = '<span dir="auto">' . $given . '</span>' . $tot;
5808
                    break;
5809
            }
5810
        }
5811
        if ($common) {
5812
            switch ($type) {
5813
                case 'table':
5814
                    global $controller;
5815
                    $table_id = Uuid::uuid4(); // lists requires a unique ID in case there are multiple lists per page
5816
                    $controller
5817
                    ->addExternalJavascript(WT_JQUERY_DATATABLES_JS_URL)
5818
                    ->addInlineJavascript('
5819
					jQuery("#' . $table_id . '").dataTable({
5820
						dom: \'t\',
5821
						autoWidth: false,
5822
						paging: false,
5823
						lengthChange: false,
5824
						filter: false,
5825
						info: false,
5826
						jQueryUI: true,
5827
						sorting: [[1,"desc"]],
5828
						columns: [
5829
							/* 0-name */ {},
5830
							/* 1-count */ { class: "center", dataSort: 2},
5831
							/* 2-COUNT */ { visible: false}
5832
						]
5833
					});
5834
					jQuery("#' . $table_id . '").css("visibility", "visible");
5835
				');
5836
                    $lookup = array('M' => I18N::translate('Male'), 'F' => I18N::translate('Female'), 'U' => I18N::translateContext('unknown gender', 'Unknown'), 'B' => I18N::translate('All'));
5837
5838
                    return '<table id="' . $table_id . '" class="givn-list"><thead><tr><th class="ui-state-default" colspan="3">' . $lookup[$sex] . '</th></tr><tr><th>' . I18N::translate('Name') . '</th><th>' . I18N::translate('Count') . '</th><th>COUNT</th></tr></thead><tbody>' . implode('', $common) . '</tbody></table>';
5839
                case 'list':
5840
                    return '<ul>' . implode('', $common) . '</ul>';
5841
                case 'nolist':
5842
                    return implode(I18N::$list_separator, $common);
5843
                default:
5844
                    return '';
5845
            }
5846
        } else {
5847
            return '';
5848
        }
5849
    }
5850
5851
    /**
5852
     * Find common give names.
5853
     *
5854
     * @param string[] $params
5855
     *
5856
     * @return string
5857
     */
5858
    public function commonGiven($params = array(1, 10, 'alpha'))
5859
    {
5860
        return $this->commonGivenQuery('B', 'nolist', false, $params);
5861
    }
5862
5863
    /**
5864
     * Find common give names.
5865
     *
5866
     * @param string[] $params
5867
     *
5868
     * @return string
5869
     */
5870
    public function commonGivenTotals($params = array(1, 10, 'rcount'))
5871
    {
5872
        return $this->commonGivenQuery('B', 'nolist', true, $params);
5873
    }
5874
5875
    /**
5876
     * Find common give names.
5877
     *
5878
     * @param string[] $params
5879
     *
5880
     * @return string
5881
     */
5882
    public function commonGivenList($params = array(1, 10, 'alpha'))
5883
    {
5884
        return $this->commonGivenQuery('B', 'list', false, $params);
5885
    }
5886
5887
    /**
5888
     * Find common give names.
5889
     *
5890
     * @param string[] $params
5891
     *
5892
     * @return string
5893
     */
5894
    public function commonGivenListTotals($params = array(1, 10, 'rcount'))
5895
    {
5896
        return $this->commonGivenQuery('B', 'list', true, $params);
5897
    }
5898
5899
    /**
5900
     * Find common give names.
5901
     *
5902
     * @param string[] $params
5903
     *
5904
     * @return string
5905
     */
5906
    public function commonGivenTable($params = array(1, 10, 'rcount'))
5907
    {
5908
        return $this->commonGivenQuery('B', 'table', false, $params);
5909
    }
5910
5911
    /**
5912
     * Find common give names of females.
5913
     *
5914
     * @param string[] $params
5915
     *
5916
     * @return string
5917
     */
5918
    public function commonGivenFemale($params = array(1, 10, 'alpha'))
5919
    {
5920
        return $this->commonGivenQuery('F', 'nolist', false, $params);
5921
    }
5922
5923
    /**
5924
     * Find common give names of females.
5925
     *
5926
     * @param string[] $params
5927
     *
5928
     * @return string
5929
     */
5930
    public function commonGivenFemaleTotals($params = array(1, 10, 'rcount'))
5931
    {
5932
        return $this->commonGivenQuery('F', 'nolist', true, $params);
5933
    }
5934
5935
    /**
5936
     * Find common give names of females.
5937
     *
5938
     * @param string[] $params
5939
     *
5940
     * @return string
5941
     */
5942
    public function commonGivenFemaleList($params = array(1, 10, 'alpha'))
5943
    {
5944
        return $this->commonGivenQuery('F', 'list', false, $params);
5945
    }
5946
5947
    /**
5948
     * Find common give names of females.
5949
     *
5950
     * @param string[] $params
5951
     *
5952
     * @return string
5953
     */
5954
    public function commonGivenFemaleListTotals($params = array(1, 10, 'rcount'))
5955
    {
5956
        return $this->commonGivenQuery('F', 'list', true, $params);
5957
    }
5958
5959
    /**
5960
     * Find common give names of females.
5961
     *
5962
     * @param string[] $params
5963
     *
5964
     * @return string
5965
     */
5966
    public function commonGivenFemaleTable($params = array(1, 10, 'rcount'))
5967
    {
5968
        return $this->commonGivenQuery('F', 'table', false, $params);
5969
    }
5970
5971
    /**
5972
     * Find common give names of males.
5973
     *
5974
     * @param string[] $params
5975
     *
5976
     * @return string
5977
     */
5978
    public function commonGivenMale($params = array(1, 10, 'alpha'))
5979
    {
5980
        return $this->commonGivenQuery('M', 'nolist', false, $params);
5981
    }
5982
5983
    /**
5984
     * Find common give names of males.
5985
     *
5986
     * @param string[] $params
5987
     *
5988
     * @return string
5989
     */
5990
    public function commonGivenMaleTotals($params = array(1, 10, 'rcount'))
5991
    {
5992
        return $this->commonGivenQuery('M', 'nolist', true, $params);
5993
    }
5994
5995
    /**
5996
     * Find common give names of males.
5997
     *
5998
     * @param string[] $params
5999
     *
6000
     * @return string
6001
     */
6002
    public function commonGivenMaleList($params = array(1, 10, 'alpha'))
6003
    {
6004
        return $this->commonGivenQuery('M', 'list', false, $params);
6005
    }
6006
6007
    /**
6008
     * Find common give names of males.
6009
     *
6010
     * @param string[] $params
6011
     *
6012
     * @return string
6013
     */
6014
    public function commonGivenMaleListTotals($params = array(1, 10, 'rcount'))
6015
    {
6016
        return $this->commonGivenQuery('M', 'list', true, $params);
6017
    }
6018
6019
    /**
6020
     * Find common give names of males.
6021
     *
6022
     * @param string[] $params
6023
     *
6024
     * @return string
6025
     */
6026
    public function commonGivenMaleTable($params = array(1, 10, 'rcount'))
6027
    {
6028
        return $this->commonGivenQuery('M', 'table', false, $params);
6029
    }
6030
6031
    /**
6032
     * Find common give names of unknown sexes.
6033
     *
6034
     * @param string[] $params
6035
     *
6036
     * @return string
6037
     */
6038
    public function commonGivenUnknown($params = array(1, 10, 'alpha'))
6039
    {
6040
        return $this->commonGivenQuery('U', 'nolist', false, $params);
6041
    }
6042
6043
    /**
6044
     * Find common give names of unknown sexes.
6045
     *
6046
     * @param string[] $params
6047
     *
6048
     * @return string
6049
     */
6050
    public function commonGivenUnknownTotals($params = array(1, 10, 'rcount'))
6051
    {
6052
        return $this->commonGivenQuery('U', 'nolist', true, $params);
6053
    }
6054
6055
    /**
6056
     * Find common give names of unknown sexes.
6057
     *
6058
     * @param string[] $params
6059
     *
6060
     * @return string
6061
     */
6062
    public function commonGivenUnknownList($params = array(1, 10, 'alpha'))
6063
    {
6064
        return $this->commonGivenQuery('U', 'list', false, $params);
6065
    }
6066
6067
    /**
6068
     * Find common give names of unknown sexes.
6069
     *
6070
     * @param string[] $params
6071
     *
6072
     * @return string
6073
     */
6074
    public function commonGivenUnknownListTotals($params = array(1, 10, 'rcount'))
6075
    {
6076
        return $this->commonGivenQuery('U', 'list', true, $params);
6077
    }
6078
6079
    /**
6080
     * Find common give names of unknown sexes.
6081
     *
6082
     * @param string[] $params
6083
     *
6084
     * @return string
6085
     */
6086
    public function commonGivenUnknownTable($params = array(1, 10, 'rcount'))
6087
    {
6088
        return $this->commonGivenQuery('U', 'table', false, $params);
6089
    }
6090
6091
    /**
6092
     * Create a chart of common given names.
6093
     *
6094
     * @param string[] $params
6095
     *
6096
     * @return string
6097
     */
6098
    public function chartCommonGiven($params = array())
6099
    {
6100
        $WT_STATS_CHART_COLOR1 = Theme::theme()->parameter('distribution-chart-no-values');
6101
        $WT_STATS_CHART_COLOR2 = Theme::theme()->parameter('distribution-chart-high-values');
6102
        $WT_STATS_S_CHART_X    = Theme::theme()->parameter('stats-small-chart-x');
6103
        $WT_STATS_S_CHART_Y    = Theme::theme()->parameter('stats-small-chart-y');
6104
6105
        if (isset($params[0]) && $params[0] != '') {
6106
            $size = strtolower($params[0]);
6107
        } else {
6108
            $size = $WT_STATS_S_CHART_X . "x" . $WT_STATS_S_CHART_Y;
6109
        }
6110
        if (isset($params[1]) && $params[1] != '') {
6111
            $color_from = strtolower($params[1]);
6112
        } else {
6113
            $color_from = $WT_STATS_CHART_COLOR1;
6114
        }
6115
        if (isset($params[2]) && $params[2] != '') {
6116
            $color_to = strtolower($params[2]);
6117
        } else {
6118
            $color_to = $WT_STATS_CHART_COLOR2;
6119
        }
6120
        if (isset($params[4]) && $params[4] != '') {
6121
            $maxtoshow = strtolower($params[4]);
6122
        } else {
6123
            $maxtoshow = 7;
6124
        }
6125
        $sizes    = explode('x', $size);
6126
        $tot_indi = $this->totalIndividualsQuery();
6127
        $given    = $this->commonGivenQuery('B', 'chart');
6128
        if (!is_array($given)) {
0 ignored issues
show
introduced by
The condition is_array($given) is always false.
Loading history...
6129
            return '';
6130
        }
6131
        $given = array_slice($given, 0, $maxtoshow);
6132
        if (count($given) <= 0) {
6133
            return '';
6134
        }
6135
        $tot = 0;
6136
        foreach ($given as $count) {
6137
            $tot += $count;
6138
        }
6139
        $chd = '';
6140
        $chl = array();
6141
        foreach ($given as $givn => $count) {
6142
            if ($tot == 0) {
6143
                $per = 0;
6144
            } else {
6145
                $per = round(100 * $count / $tot_indi, 0);
6146
            }
6147
            $chd .= $this->arrayToExtendedEncoding(array($per));
6148
            $chl[] = $givn . ' - ' . I18N::number($count);
6149
        }
6150
        $per = round(100 * ($tot_indi - $tot) / $tot_indi, 0);
6151
        $chd .= $this->arrayToExtendedEncoding(array($per));
6152
        $chl[] = I18N::translate('Other') . ' - ' . I18N::number($tot_indi - $tot);
6153
6154
        $chart_title = implode(I18N::$list_separator, $chl);
6155
        $chl         = implode('|', $chl);
6156
6157
        return "<img src=\"https://chart.googleapis.com/chart?cht=p3&amp;chd=e:{$chd}&amp;chs={$size}&amp;chco={$color_from},{$color_to}&amp;chf=bg,s,ffffff00&amp;chl=" . rawurlencode($chl) . "\" width=\"{$sizes[0]}\" height=\"{$sizes[1]}\" alt=\"" . $chart_title . "\" title=\"" . $chart_title . "\" />";
6158
    }
6159
6160
    /**
6161
     * Who is currently logged in?
6162
     *
6163
     * @param string $type
6164
     *
6165
     * @return string
6166
     */
6167
    private function usersLoggedInQuery($type = 'nolist')
6168
    {
6169
        $content = '';
6170
        // List active users
6171
        $NumAnonymous = 0;
6172
        $loggedusers  = array();
6173
        foreach (User::allLoggedIn() as $user) {
6174
            if (Auth::isAdmin() || $user->getPreference('visibleonline')) {
6175
                $loggedusers[] = $user;
6176
            } else {
6177
                $NumAnonymous++;
6178
            }
6179
        }
6180
        $LoginUsers = count($loggedusers);
6181
        if ($LoginUsers == 0 && $NumAnonymous == 0) {
6182
            return I18N::translate('No signed-in and no anonymous users');
6183
        }
6184
        if ($NumAnonymous > 0) {
6185
            $content .= '<b>' . I18N::plural('%s anonymous signed-in user', '%s anonymous signed-in users', $NumAnonymous, I18N::number($NumAnonymous)) . '</b>';
6186
        }
6187
        if ($LoginUsers > 0) {
6188
            if ($NumAnonymous) {
6189
                if ($type == 'list') {
6190
                    $content .= "<br><br>";
6191
                } else {
6192
                    $content .= " " . I18N::translate('and') . " ";
6193
                }
6194
            }
6195
            $content .= '<b>' . I18N::plural('%s signed-in user', '%s signed-in users', $LoginUsers, I18N::number($LoginUsers)) . '</b>';
6196
            if ($type == 'list') {
6197
                $content .= '<ul>';
6198
            } else {
6199
                $content .= ': ';
6200
            }
6201
        }
6202
        if (Auth::check()) {
6203
            foreach ($loggedusers as $user) {
6204
                if ($type == 'list') {
6205
                    $content .= '<li>' . Filter::escapeHtml($user->getRealName()) . ' - ' . Filter::escapeHtml($user->getUserName());
6206
                } else {
6207
                    $content .= Filter::escapeHtml($user->getRealName()) . ' - ' . Filter::escapeHtml($user->getUserName());
6208
                }
6209
                if (Auth::id() != $user->getUserId() && $user->getPreference('contactmethod') != 'none') {
6210
                    if ($type == 'list') {
6211
                        $content .= '<br><a class="icon-email" href="#" onclick="return message(\'' . $user->getUserId() . '\', \'\', \'' . Filter::escapeJs(Functions::getQueryUrl()) . '\');" title="' . I18N::translate('Send a message') . '"></a>';
6212
                    } else {
6213
                        $content .= ' <a class="icon-email" href="#" onclick="return message(\'' . $user->getUserId() . '\', \'\', \'' . Filter::escapeJs(Functions::getQueryUrl()) . '\');" title="' . I18N::translate('Send a message') . '"></a>';
6214
                    }
6215
                }
6216
                if ($type == 'list') {
6217
                    $content .= '</li>';
6218
                }
6219
            }
6220
        }
6221
        if ($type == 'list') {
6222
            $content .= '</ul>';
6223
        }
6224
6225
        return $content;
6226
    }
6227
6228
    /**
6229
     * NUmber of users who are currently logged in?
6230
     *
6231
     * @param string $type
6232
     *
6233
     * @return int
6234
     */
6235
    private function usersLoggedInTotalQuery($type = 'all')
6236
    {
6237
        $anon    = 0;
6238
        $visible = 0;
6239
        foreach (User::allLoggedIn() as $user) {
6240
            if (Auth::isAdmin() || $user->getPreference('visibleonline')) {
6241
                $visible++;
6242
            } else {
6243
                $anon++;
6244
            }
6245
        }
6246
        if ($type == 'anon') {
6247
            return $anon;
6248
        } elseif ($type == 'visible') {
6249
            return $visible;
6250
        } else {
6251
            return $visible + $anon;
6252
        }
6253
    }
6254
6255
    /**
6256
     * Who is currently logged in?
6257
     *
6258
     * @return string
6259
     */
6260
    public function usersLoggedIn()
6261
    {
6262
        return $this->usersLoggedInQuery('nolist');
6263
    }
6264
6265
    /**
6266
     * Who is currently logged in?
6267
     *
6268
     * @return string
6269
     */
6270
    public function usersLoggedInList()
6271
    {
6272
        return $this->usersLoggedInQuery('list');
6273
    }
6274
6275
    /**
6276
     * Who is currently logged in?
6277
     *
6278
     * @return int
6279
     */
6280
    public function usersLoggedInTotal()
6281
    {
6282
        return $this->usersLoggedInTotalQuery('all');
6283
    }
6284
6285
    /**
6286
     * Which visitors are currently logged in?
6287
     *
6288
     * @return int
6289
     */
6290
    public function usersLoggedInTotalAnon()
6291
    {
6292
        return $this->usersLoggedInTotalQuery('anon');
6293
    }
6294
6295
    /**
6296
     * Which visitors are currently logged in?
6297
     *
6298
     * @return int
6299
     */
6300
    public function usersLoggedInTotalVisible()
6301
    {
6302
        return $this->usersLoggedInTotalQuery('visible');
6303
    }
6304
6305
    /**
6306
     * Get the current user's ID.
6307
     *
6308
     * @return null|string
6309
     */
6310
    public function userId()
6311
    {
6312
        return Auth::id();
6313
    }
6314
6315
    /**
6316
     * Get the current user's username.
6317
     *
6318
     * @param string[] $params
6319
     *
6320
     * @return string
6321
     */
6322
    public function userName($params = array())
6323
    {
6324
        if (Auth::check()) {
6325
            return Filter::escapeHtml(Auth::user()->getUserName());
6326
        } elseif (isset($params[0]) && $params[0] != '') {
6327
            // if #username:visitor# was specified, then "visitor" will be returned when the user is not logged in
6328
            return Filter::escapeHtml($params[0]);
6329
        } else {
6330
            return '';
6331
        }
6332
    }
6333
6334
    /**
6335
     * Get the current user's full name.
6336
     *
6337
     * @return string
6338
     */
6339
    public function userFullName()
6340
    {
6341
        return Auth::check() ? Auth::user()->getRealNameHtml() : '';
6342
    }
6343
6344
    /**
6345
     * Get the newest registered user.
6346
     *
6347
     * @param string   $type
6348
     * @param string[] $params
6349
     *
6350
     * @return string
6351
     */
6352
    private function getLatestUserData($type = 'userid', $params = array())
6353
    {
6354
        static $user_id = null;
6355
6356
        if ($user_id === null) {
6357
            $user = User::findLatestToRegister();
6358
        } else {
6359
            $user = User::find($user_id);
6360
        }
6361
6362
        switch ($type) {
6363
            default:
6364
            case 'userid':
6365
                return $user->getUserId();
6366
            case 'username':
6367
                return Filter::escapeHtml($user->getUserName());
6368
            case 'fullname':
6369
                return $user->getRealNameHtml();
6370
            case 'regdate':
6371
                if (is_array($params) && isset($params[0]) && $params[0] != '') {
6372
                    $datestamp = $params[0];
6373
                } else {
6374
                    $datestamp = I18N::dateFormat();
6375
                }
6376
6377
                return FunctionsDate::timestampToGedcomDate($user->getPreference('reg_timestamp'))->display(false, $datestamp);
0 ignored issues
show
Bug introduced by
It seems like $user->getPreference('reg_timestamp') can also be of type string; however, parameter $time of Fisharebest\Webtrees\Fun...timestampToGedcomDate() does only seem to accept integer, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

6377
                return FunctionsDate::timestampToGedcomDate(/** @scrutinizer ignore-type */ $user->getPreference('reg_timestamp'))->display(false, $datestamp);
Loading history...
6378
            case 'regtime':
6379
                if (is_array($params) && isset($params[0]) && $params[0] != '') {
6380
                    $datestamp = $params[0];
6381
                } else {
6382
                    $datestamp = str_replace('%', '', I18N::timeFormat());
6383
                }
6384
6385
                return date($datestamp, $user->getPreference('reg_timestamp'));
0 ignored issues
show
Bug introduced by
It seems like $user->getPreference('reg_timestamp') can also be of type string; however, parameter $timestamp of date() does only seem to accept integer|null, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

6385
                return date($datestamp, /** @scrutinizer ignore-type */ $user->getPreference('reg_timestamp'));
Loading history...
6386
            case 'loggedin':
6387
                if (is_array($params) && isset($params[0]) && $params[0] != '') {
6388
                    $yes = $params[0];
6389
                } else {
6390
                    $yes = I18N::translate('yes');
6391
                }
6392
                if (is_array($params) && isset($params[1]) && $params[1] != '') {
6393
                    $no = $params[1];
6394
                } else {
6395
                    $no = I18N::translate('no');
6396
                }
6397
6398
                return Database::prepare("SELECT 1 FROM `##session` WHERE user_id=? LIMIT 1")->execute(array($user->getUserId()))->fetchOne() ? $yes : $no;
6399
        }
6400
    }
6401
6402
    /**
6403
     * Get the newest registered user's ID.
6404
     *
6405
     * @return string
6406
     */
6407
    public function latestUserId()
6408
    {
6409
        return $this->getLatestUserData('userid');
6410
    }
6411
6412
    /**
6413
     * Get the newest registered user's username.
6414
     *
6415
     * @return string
6416
     */
6417
    public function latestUserName()
6418
    {
6419
        return $this->getLatestUserData('username');
6420
    }
6421
6422
    /**
6423
     * Get the newest registered user's real name.
6424
     *
6425
     * @return string
6426
     */
6427
    public function latestUserFullName()
6428
    {
6429
        return $this->getLatestUserData('fullname');
6430
    }
6431
6432
    /**
6433
     * Get the date of the newest user registration.
6434
     *
6435
     * @param string[] $params
6436
     *
6437
     * @return string
6438
     */
6439
    public function latestUserRegDate($params = array())
6440
    {
6441
        return $this->getLatestUserData('regdate', $params);
6442
    }
6443
6444
    /**
6445
     * Find the timestamp of the latest user to register.
6446
     *
6447
     * @param string[] $params
6448
     *
6449
     * @return string
6450
     */
6451
    public function latestUserRegTime($params = array())
6452
    {
6453
        return $this->getLatestUserData('regtime', $params);
6454
    }
6455
6456
    /**
6457
     * Find the most recent user to log in.
6458
     *
6459
     * @param string[] $params
6460
     *
6461
     * @return string
6462
     */
6463
    public function latestUserLoggedin($params = array())
6464
    {
6465
        return $this->getLatestUserData('loggedin', $params);
6466
    }
6467
6468
    /**
6469
     * Create a link to contact the webmaster.
6470
     *
6471
     * @return string
6472
     */
6473
    public function contactWebmaster()
6474
    {
6475
        $user_id = $this->tree->getPreference('WEBMASTER_USER_ID');
6476
        $user    = User::find($user_id);
0 ignored issues
show
Bug introduced by
It seems like $user_id can also be of type string; however, parameter $user_id of Fisharebest\Webtrees\User::find() does only seem to accept integer|null, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

6476
        $user    = User::find(/** @scrutinizer ignore-type */ $user_id);
Loading history...
6477
        if ($user) {
0 ignored issues
show
introduced by
$user is of type Fisharebest\Webtrees\User, thus it always evaluated to true.
Loading history...
6478
            return Theme::theme()->contactLink($user);
6479
        } else {
6480
            return $user_id;
6481
        }
6482
    }
6483
6484
    /**
6485
     * Create a link to contact the genealogy contact.
6486
     *
6487
     * @return string
6488
     */
6489
    public function contactGedcom()
6490
    {
6491
        $user_id = $this->tree->getPreference('CONTACT_USER_ID');
6492
        $user    = User::find($user_id);
0 ignored issues
show
Bug introduced by
It seems like $user_id can also be of type string; however, parameter $user_id of Fisharebest\Webtrees\User::find() does only seem to accept integer|null, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

6492
        $user    = User::find(/** @scrutinizer ignore-type */ $user_id);
Loading history...
6493
        if ($user) {
0 ignored issues
show
introduced by
$user is of type Fisharebest\Webtrees\User, thus it always evaluated to true.
Loading history...
6494
            return Theme::theme()->contactLink($user);
6495
        } else {
6496
            return $user_id;
6497
        }
6498
    }
6499
6500
    /**
6501
     * What is the current date on the server?
6502
     *
6503
     * @return string
6504
     */
6505
    public function serverDate()
6506
    {
6507
        return FunctionsDate::timestampToGedcomDate(WT_TIMESTAMP)->display();
6508
    }
6509
6510
    /**
6511
     * What is the current time on the server (in 12 hour clock)?
6512
     *
6513
     * @return string
6514
     */
6515
    public function serverTime()
6516
    {
6517
        return date('g:i a');
6518
    }
6519
6520
    /**
6521
     * What is the current time on the server (in 24 hour clock)?
6522
     *
6523
     * @return string
6524
     */
6525
    public function serverTime24()
6526
    {
6527
        return date('G:i');
6528
    }
6529
6530
    /**
6531
     * What is the timezone of the server.
6532
     *
6533
     * @return string
6534
     */
6535
    public function serverTimezone()
6536
    {
6537
        return date('T');
6538
    }
6539
6540
    /**
6541
     * What is the client's date.
6542
     *
6543
     * @return string
6544
     */
6545
    public function browserDate()
6546
    {
6547
        return FunctionsDate::timestampToGedcomDate(WT_TIMESTAMP + WT_TIMESTAMP_OFFSET)->display();
6548
    }
6549
6550
    /**
6551
     * What is the client's timestamp.
6552
     *
6553
     * @return string
6554
     */
6555
    public function browserTime()
6556
    {
6557
        return date(str_replace('%', '', I18N::timeFormat()), WT_TIMESTAMP + WT_TIMESTAMP_OFFSET);
6558
    }
6559
6560
    /**
6561
     * What is the browser's tiemzone.
6562
     *
6563
     * @return string
6564
     */
6565
    public function browserTimezone()
6566
    {
6567
        return date('T', WT_TIMESTAMP + WT_TIMESTAMP_OFFSET);
6568
    }
6569
6570
    /**
6571
     * What is the current version of webtrees.
6572
     *
6573
     * @return string
6574
     */
6575
    public function webtreesVersion()
6576
    {
6577
        return WT_VERSION;
6578
    }
6579
6580
    /**
6581
     * These functions provide access to hitcounter for use in the HTML block.
6582
     *
6583
     * @param string   $page_name
6584
     * @param string[] $params
6585
     *
6586
     * @return string
6587
     */
6588
    private function hitCountQuery($page_name, $params)
6589
    {
6590
        if (is_array($params) && isset($params[0]) && $params[0] != '') {
6591
            $page_parameter = $params[0];
6592
        } else {
6593
            $page_parameter = '';
6594
        }
6595
6596
        if ($page_name === null) {
0 ignored issues
show
introduced by
The condition $page_name === null is always false.
Loading history...
6597
            // index.php?ctype=gedcom
6598
            $page_name      = 'index.php';
6599
            $page_parameter = 'gedcom:' . ($page_parameter ? Tree::findByName($page_parameter)->getTreeId() : $this->tree->getTreeId());
6600
        } elseif ($page_name == 'index.php') {
6601
            // index.php?ctype=user
6602
            $user           = User::findByIdentifier($page_parameter);
6603
            $page_parameter = 'user:' . ($user ? $user->getUserId() : Auth::id());
0 ignored issues
show
introduced by
$user is of type Fisharebest\Webtrees\User, thus it always evaluated to true.
Loading history...
6604
        } else {
6605
            // indi/fam/sour/etc.
6606
        }
6607
6608
        return '<span class="odometer">' . I18N::digits(HitCounter::getCount($this->tree, $page_name, $page_parameter)) . '</span>';
6609
    }
6610
6611
    /**
6612
     * How many times has a page been viewed.
6613
     *
6614
     * @param string[] $params
6615
     *
6616
     * @return string
6617
     */
6618
    public function hitCount($params = array())
6619
    {
6620
        return $this->hitCountQuery(null, $params);
6621
    }
6622
6623
    /**
6624
     * How many times has a page been viewed.
6625
     *
6626
     * @param string[] $params
6627
     *
6628
     * @return string
6629
     */
6630
    public function hitCountUser($params = array())
6631
    {
6632
        return $this->hitCountQuery('index.php', $params);
6633
    }
6634
6635
    /**
6636
     * How many times has a page been viewed.
6637
     *
6638
     * @param string[] $params
6639
     *
6640
     * @return string
6641
     */
6642
    public function hitCountIndi($params = array())
6643
    {
6644
        return $this->hitCountQuery('individual.php', $params);
6645
    }
6646
6647
    /**
6648
     * How many times has a page been viewed.
6649
     *
6650
     * @param string[] $params
6651
     *
6652
     * @return string
6653
     */
6654
    public function hitCountFam($params = array())
6655
    {
6656
        return $this->hitCountQuery('family.php', $params);
6657
    }
6658
6659
    /**
6660
     * How many times has a page been viewed.
6661
     *
6662
     * @param string[] $params
6663
     *
6664
     * @return string
6665
     */
6666
    public function hitCountSour($params = array())
6667
    {
6668
        return $this->hitCountQuery('source.php', $params);
6669
    }
6670
6671
    /**
6672
     * How many times has a page been viewed.
6673
     *
6674
     * @param string[] $params
6675
     *
6676
     * @return string
6677
     */
6678
    public function hitCountRepo($params = array())
6679
    {
6680
        return $this->hitCountQuery('repo.php', $params);
6681
    }
6682
6683
    /**
6684
     * How many times has a page been viewed.
6685
     *
6686
     * @param string[] $params
6687
     *
6688
     * @return string
6689
     */
6690
    public function hitCountNote($params = array())
6691
    {
6692
        return $this->hitCountQuery('note.php', $params);
6693
    }
6694
6695
    /**
6696
     * How many times has a page been viewed.
6697
     *
6698
     * @param string[] $params
6699
     *
6700
     * @return string
6701
     */
6702
    public function hitCountObje($params = array())
6703
    {
6704
        return $this->hitCountQuery('mediaviewer.php', $params);
6705
    }
6706
6707
    /**
6708
     * Convert numbers to Google's custom encoding.
6709
     *
6710
     * @link http://bendodson.com/news/google-extended-encoding-made-easy
6711
     *
6712
     * @param int[] $a
6713
     *
6714
     * @return string
6715
     */
6716
    private function arrayToExtendedEncoding($a)
6717
    {
6718
        $xencoding = WT_GOOGLE_CHART_ENCODING;
6719
6720
        $encoding = '';
6721
        foreach ($a as $value) {
6722
            if ($value < 0) {
6723
                $value = 0;
6724
            }
6725
            $first  = (int) ($value / 64);
6726
            $second = $value % 64;
6727
            $encoding .= $xencoding[(int) $first] . $xencoding[(int) $second];
6728
        }
6729
6730
        return $encoding;
6731
    }
6732
6733
    /**
6734
     * Callback function to compare totals.
6735
     *
6736
     * @param array $a
6737
     * @param array $b
6738
     *
6739
     * @return int
6740
     */
6741
    private function nameTotalSort($a, $b)
6742
    {
6743
        return $a['match'] - $b['match'];
6744
    }
6745
6746
    /**
6747
     * Callback function to compare totals.
6748
     *
6749
     * @param array $a
6750
     * @param array $b
6751
     *
6752
     * @return int
6753
     */
6754
    private function nameTotalReverseSort($a, $b)
6755
    {
6756
        return $b['match'] - $a['match'];
6757
    }
6758
6759
    /**
6760
     * Run an SQL query and cache the result.
6761
     *
6762
     * @param string $sql
6763
     *
6764
     * @return string[][]
6765
     */
6766
    private function runSql($sql)
6767
    {
6768
        static $cache = array();
6769
6770
        $id = md5($sql);
6771
        if (isset($cache[$id])) {
6772
            return $cache[$id];
6773
        }
6774
        $rows       = Database::prepare($sql)->fetchAll(PDO::FETCH_ASSOC);
6775
        $cache[$id] = $rows;
6776
6777
        return $rows;
6778
    }
6779
6780
    /**
6781
     * Find the favorites for the tree.
6782
     *
6783
     * @return string
6784
     */
6785
    public function gedcomFavorites()
6786
    {
6787
        if (Module::getModuleByName('gedcom_favorites')) {
6788
            $block = new FamilyTreeFavoritesModule(WT_MODULES_DIR . 'gedcom_favorites');
6789
6790
            return $block->getBlock(0, false);
6791
        } else {
6792
            return '';
6793
        }
6794
    }
6795
6796
    /**
6797
     * Find the favorites for the user.
6798
     *
6799
     * @return string
6800
     */
6801
    public function userFavorites()
6802
    {
6803
        if (Auth::check() && Module::getModuleByName('user_favorites')) {
6804
            $block = new UserFavoritesModule(WT_MODULES_DIR . 'gedcom_favorites');
6805
6806
            return $block->getBlock(0, false);
6807
        } else {
6808
            return '';
6809
        }
6810
    }
6811
6812
    /**
6813
     * Find the number of favorites for the tree.
6814
     *
6815
     * @return int
6816
     */
6817
    public function totalGedcomFavorites()
6818
    {
6819
        if (Module::getModuleByName('gedcom_favorites')) {
6820
            return count(FamilyTreeFavoritesModule::getFavorites($this->tree->getTreeId()));
6821
        } else {
6822
            return 0;
6823
        }
6824
    }
6825
6826
    /**
6827
     * Find the number of favorites for the user.
6828
     *
6829
     * @return int
6830
     */
6831
    public function totalUserFavorites()
6832
    {
6833
        if (Module::getModuleByName('user_favorites')) {
6834
            return count(UserFavoritesModule::getFavorites(Auth::id()));
0 ignored issues
show
Bug introduced by
It seems like Fisharebest\Webtrees\Auth::id() can also be of type string; however, parameter $user_id of Fisharebest\Webtrees\Mod...sModule::getFavorites() does only seem to accept integer, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

6834
            return count(UserFavoritesModule::getFavorites(/** @scrutinizer ignore-type */ Auth::id()));
Loading history...
6835
        } else {
6836
            return 0;
6837
        }
6838
    }
6839
6840
    /**
6841
     * Create any of the other blocks.
6842
     *
6843
     * Use as #callBlock:block_name#
6844
     *
6845
     * @param string[] $params
6846
     *
6847
     * @return string
6848
     */
6849
    public function callBlock($params = array())
6850
    {
6851
        global $ctype;
6852
6853
        if (isset($params[0]) && $params[0] != '') {
6854
            $block = $params[0];
6855
        } else {
6856
            return '';
6857
        }
6858
        $all_blocks = array();
6859
        foreach (Module::getActiveBlocks($this->tree) as $name => $active_block) {
6860
            if ($ctype == 'user' && $active_block->isUserBlock() || $ctype == 'gedcom' && $active_block->isGedcomBlock()) {
0 ignored issues
show
introduced by
Consider adding parentheses for clarity. Current Interpretation: ($ctype == 'user' && $ac..._block->isGedcomBlock(), Probably Intended Meaning: $ctype == 'user' && ($ac...block->isGedcomBlock())
Loading history...
6861
                $all_blocks[$name] = $active_block;
6862
            }
6863
        }
6864
        if (!array_key_exists($block, $all_blocks) || $block == 'html') {
6865
            return '';
6866
        }
6867
        // Build the config array
6868
        array_shift($params);
6869
        $cfg = array();
6870
        foreach ($params as $config) {
6871
            $bits = explode('=', $config);
6872
            if (count($bits) < 2) {
6873
                continue;
6874
            }
6875
            $v       = array_shift($bits);
6876
            $cfg[$v] = implode('=', $bits);
6877
        }
6878
        $block    = $all_blocks[$block];
6879
        $block_id = Filter::getInteger('block_id');
6880
        $content  = $block->getBlock($block_id, false, $cfg);
6881
6882
        return $content;
6883
    }
6884
6885
    /**
6886
     * How many messages in the user's inbox.
6887
     *
6888
     * @return string
6889
     */
6890
    public function totalUserMessages()
6891
    {
6892
        $total = (int) Database::prepare("SELECT COUNT(*) FROM `##message` WHERE user_id = ?")
6893
            ->execute(array(Auth::id()))
6894
            ->fetchOne();
6895
6896
        return I18N::number($total);
6897
    }
6898
6899
    /**
6900
     * How many blog entries exist for this user.
6901
     *
6902
     * @return string
6903
     */
6904
    public function totalUserJournal()
6905
    {
6906
        try {
6907
            $number = (int) Database::prepare("SELECT COUNT(*) FROM `##news` WHERE user_id = ?")
6908
                ->execute(array(Auth::id()))
6909
                ->fetchOne();
6910
        } catch (PDOException $ex) {
6911
            // The module may not be installed, so the table may not exist.
6912
            $number = 0;
6913
        }
6914
6915
        return I18N::number($number);
6916
    }
6917
6918
    /**
6919
     * How many news items exist for this tree.
6920
     *
6921
     * @return string
6922
     */
6923
    public function totalGedcomNews()
6924
    {
6925
        try {
6926
            $number = (int) Database::prepare("SELECT COUNT(*) FROM `##news` WHERE gedcom_id = ?")
6927
                ->execute(array($this->tree->getTreeId()))
6928
                ->fetchOne();
6929
        } catch (PDOException $ex) {
6930
            // The module may not be installed, so the table may not exist.
6931
            $number = 0;
6932
        }
6933
6934
        return I18N::number($number);
6935
    }
6936
6937
    /**
6938
     * ISO3166 3 letter codes, with their 2 letter equivalent.
6939
     * NOTE: this is not 1:1. ENG/SCO/WAL/NIR => GB
6940
     * NOTE: this also includes champman codes and others. Should it?
6941
     *
6942
     * @return string[]
6943
     */
6944
    public function iso3166()
6945
    {
6946
        return array(
6947
            'ABW' => 'AW', 'AFG' => 'AF', 'AGO' => 'AO', 'AIA' => 'AI', 'ALA' => 'AX', 'ALB' => 'AL',
6948
            'AND' => 'AD', 'ARE' => 'AE', 'ARG' => 'AR', 'ARM' => 'AM', 'ASM' => 'AS',
6949
            'ATA' => 'AQ', 'ATF' => 'TF', 'ATG' => 'AG', 'AUS' => 'AU', 'AUT' => 'AT', 'AZE' => 'AZ',
6950
            'BDI' => 'BI', 'BEL' => 'BE', 'BEN' => 'BJ', 'BFA' => 'BF', 'BGD' => 'BD', 'BGR' => 'BG',
6951
            'BHR' => 'BH', 'BHS' => 'BS', 'BIH' => 'BA', 'BLR' => 'BY', 'BLZ' => 'BZ', 'BMU' => 'BM',
6952
            'BOL' => 'BO', 'BRA' => 'BR', 'BRB' => 'BB', 'BRN' => 'BN', 'BTN' => 'BT', 'BVT' => 'BV',
6953
            'BWA' => 'BW', 'CAF' => 'CF', 'CAN' => 'CA', 'CCK' => 'CC', 'CHE' => 'CH', 'CHL' => 'CL',
6954
            'CHN' => 'CN', 'CIV' => 'CI', 'CMR' => 'CM', 'COD' => 'CD', 'COG' => 'CG',
6955
            'COK' => 'CK', 'COL' => 'CO', 'COM' => 'KM', 'CPV' => 'CV', 'CRI' => 'CR', 'CUB' => 'CU',
6956
            'CXR' => 'CX', 'CYM' => 'KY', 'CYP' => 'CY', 'CZE' => 'CZ', 'DEU' => 'DE', 'DJI' => 'DJ',
6957
            'DMA' => 'DM', 'DNK' => 'DK', 'DOM' => 'DO', 'DZA' => 'DZ', 'ECU' => 'EC', 'EGY' => 'EG',
6958
            'ENG' => 'GB', 'ERI' => 'ER', 'ESH' => 'EH', 'ESP' => 'ES', 'EST' => 'EE', 'ETH' => 'ET',
6959
            'FIN' => 'FI', 'FJI' => 'FJ', 'FLK' => 'FK', 'FRA' => 'FR', 'FRO' => 'FO', 'FSM' => 'FM',
6960
            'GAB' => 'GA', 'GBR' => 'GB', 'GEO' => 'GE', 'GHA' => 'GH', 'GIB' => 'GI', 'GIN' => 'GN',
6961
            'GLP' => 'GP', 'GMB' => 'GM', 'GNB' => 'GW', 'GNQ' => 'GQ', 'GRC' => 'GR', 'GRD' => 'GD',
6962
            'GRL' => 'GL', 'GTM' => 'GT', 'GUF' => 'GF', 'GUM' => 'GU', 'GUY' => 'GY', 'HKG' => 'HK',
6963
            'HMD' => 'HM', 'HND' => 'HN', 'HRV' => 'HR', 'HTI' => 'HT', 'HUN' => 'HU', 'IDN' => 'ID',
6964
            'IND' => 'IN', 'IOT' => 'IO', 'IRL' => 'IE', 'IRN' => 'IR', 'IRQ' => 'IQ', 'ISL' => 'IS',
6965
            'ISR' => 'IL', 'ITA' => 'IT', 'JAM' => 'JM', 'JOR' => 'JO', 'JPN' => 'JA', 'KAZ' => 'KZ',
6966
            'KEN' => 'KE', 'KGZ' => 'KG', 'KHM' => 'KH', 'KIR' => 'KI', 'KNA' => 'KN', 'KOR' => 'KO',
6967
            'KWT' => 'KW', 'LAO' => 'LA', 'LBN' => 'LB', 'LBR' => 'LR', 'LBY' => 'LY', 'LCA' => 'LC',
6968
            'LIE' => 'LI', 'LKA' => 'LK', 'LSO' => 'LS', 'LTU' => 'LT', 'LUX' => 'LU', 'LVA' => 'LV',
6969
            'MAC' => 'MO', 'MAR' => 'MA', 'MCO' => 'MC', 'MDA' => 'MD', 'MDG' => 'MG', 'MDV' => 'MV',
6970
            'MEX' => 'MX', 'MHL' => 'MH', 'MKD' => 'MK', 'MLI' => 'ML', 'MLT' => 'MT', 'MMR' => 'MM',
6971
            'MNG' => 'MN', 'MNP' => 'MP', 'MNT' => 'ME', 'MOZ' => 'MZ', 'MRT' => 'MR', 'MSR' => 'MS',
6972
            'MTQ' => 'MQ', 'MUS' => 'MU', 'MWI' => 'MW', 'MYS' => 'MY', 'MYT' => 'YT', 'NAM' => 'NA',
6973
            'NCL' => 'NC', 'NER' => 'NE', 'NFK' => 'NF', 'NGA' => 'NG', 'NIC' => 'NI', 'NIR' => 'GB',
6974
            'NIU' => 'NU', 'NLD' => 'NL', 'NOR' => 'NO', 'NPL' => 'NP', 'NRU' => 'NR', 'NZL' => 'NZ',
6975
            'OMN' => 'OM', 'PAK' => 'PK', 'PAN' => 'PA', 'PCN' => 'PN', 'PER' => 'PE', 'PHL' => 'PH',
6976
            'PLW' => 'PW', 'PNG' => 'PG', 'POL' => 'PL', 'PRI' => 'PR', 'PRK' => 'KP', 'PRT' => 'PO',
6977
            'PRY' => 'PY', 'PSE' => 'PS', 'PYF' => 'PF', 'QAT' => 'QA', 'REU' => 'RE', 'ROM' => 'RO',
6978
            'RUS' => 'RU', 'RWA' => 'RW', 'SAU' => 'SA', 'SCT' => 'GB', 'SDN' => 'SD', 'SEN' => 'SN',
6979
            'SER' => 'RS', 'SGP' => 'SG', 'SGS' => 'GS', 'SHN' => 'SH', 'SJM' => 'SJ',
6980
            'SLB' => 'SB', 'SLE' => 'SL', 'SLV' => 'SV', 'SMR' => 'SM', 'SOM' => 'SO', 'SPM' => 'PM',
6981
            'STP' => 'ST', 'SUR' => 'SR', 'SVK' => 'SK', 'SVN' => 'SI', 'SWE' => 'SE',
6982
            'SWZ' => 'SZ', 'SYC' => 'SC', 'SYR' => 'SY', 'TCA' => 'TC', 'TCD' => 'TD', 'TGO' => 'TG',
6983
            'THA' => 'TH', 'TJK' => 'TJ', 'TKL' => 'TK', 'TKM' => 'TM', 'TLS' => 'TL', 'TON' => 'TO',
6984
            'TTO' => 'TT', 'TUN' => 'TN', 'TUR' => 'TR', 'TUV' => 'TV', 'TWN' => 'TW', 'TZA' => 'TZ',
6985
            'UGA' => 'UG', 'UKR' => 'UA', 'UMI' => 'UM', 'URY' => 'UY', 'USA' => 'US', 'UZB' => 'UZ',
6986
            'VAT' => 'VA', 'VCT' => 'VC', 'VEN' => 'VE', 'VGB' => 'VG', 'VIR' => 'VI', 'VNM' => 'VN',
6987
            'VUT' => 'VU', 'WLF' => 'WF', 'WLS' => 'GB', 'WSM' => 'WS', 'YEM' => 'YE', 'ZAF' => 'ZA',
6988
            'ZMB' => 'ZM', 'ZWE' => 'ZW',
6989
        );
6990
    }
6991
6992
    /**
6993
     * Country codes and names
6994
     *
6995
     * @return string[]
6996
     */
6997
    public function getAllCountries()
6998
    {
6999
        return array(
7000
            '???' => /* I18N: Name of a country or state */ I18N::translate('Unknown'),
7001
            'ABW' => /* I18N: Name of a country or state */ I18N::translate('Aruba'),
7002
            'AFG' => /* I18N: Name of a country or state */ I18N::translate('Afghanistan'),
7003
            'AGO' => /* I18N: Name of a country or state */ I18N::translate('Angola'),
7004
            'AIA' => /* I18N: Name of a country or state */ I18N::translate('Anguilla'),
7005
            'ALA' => /* I18N: Name of a country or state */ I18N::translate('Aland Islands'),
7006
            'ALB' => /* I18N: Name of a country or state */ I18N::translate('Albania'),
7007
            'AND' => /* I18N: Name of a country or state */ I18N::translate('Andorra'),
7008
            'ARE' => /* I18N: Name of a country or state */ I18N::translate('United Arab Emirates'),
7009
            'ARG' => /* I18N: Name of a country or state */ I18N::translate('Argentina'),
7010
            'ARM' => /* I18N: Name of a country or state */ I18N::translate('Armenia'),
7011
            'ASM' => /* I18N: Name of a country or state */ I18N::translate('American Samoa'),
7012
            'ATA' => /* I18N: Name of a country or state */ I18N::translate('Antarctica'),
7013
            'ATF' => /* I18N: Name of a country or state */ I18N::translate('French Southern Territories'),
7014
            'ATG' => /* I18N: Name of a country or state */ I18N::translate('Antigua and Barbuda'),
7015
            'AUS' => /* I18N: Name of a country or state */ I18N::translate('Australia'),
7016
            'AUT' => /* I18N: Name of a country or state */ I18N::translate('Austria'),
7017
            'AZE' => /* I18N: Name of a country or state */ I18N::translate('Azerbaijan'),
7018
            'AZR' => /* I18N: Name of a country or state */ I18N::translate('Azores'),
7019
            'BDI' => /* I18N: Name of a country or state */ I18N::translate('Burundi'),
7020
            'BEL' => /* I18N: Name of a country or state */ I18N::translate('Belgium'),
7021
            'BEN' => /* I18N: Name of a country or state */ I18N::translate('Benin'),
7022
            // BES => Bonaire, Sint Eustatius and Saba
7023
            'BFA' => /* I18N: Name of a country or state */ I18N::translate('Burkina Faso'),
7024
            'BGD' => /* I18N: Name of a country or state */ I18N::translate('Bangladesh'),
7025
            'BGR' => /* I18N: Name of a country or state */ I18N::translate('Bulgaria'),
7026
            'BHR' => /* I18N: Name of a country or state */ I18N::translate('Bahrain'),
7027
            'BHS' => /* I18N: Name of a country or state */ I18N::translate('Bahamas'),
7028
            'BIH' => /* I18N: Name of a country or state */ I18N::translate('Bosnia and Herzegovina'),
7029
            // BLM => Saint Barthélemy
7030
            'BLR' => /* I18N: Name of a country or state */ I18N::translate('Belarus'),
7031
            'BLZ' => /* I18N: Name of a country or state */ I18N::translate('Belize'),
7032
            'BMU' => /* I18N: Name of a country or state */ I18N::translate('Bermuda'),
7033
            'BOL' => /* I18N: Name of a country or state */ I18N::translate('Bolivia'),
7034
            'BRA' => /* I18N: Name of a country or state */ I18N::translate('Brazil'),
7035
            'BRB' => /* I18N: Name of a country or state */ I18N::translate('Barbados'),
7036
            'BRN' => /* I18N: Name of a country or state */ I18N::translate('Brunei Darussalam'),
7037
            'BTN' => /* I18N: Name of a country or state */ I18N::translate('Bhutan'),
7038
            'BVT' => /* I18N: Name of a country or state */ I18N::translate('Bouvet Island'),
7039
            'BWA' => /* I18N: Name of a country or state */ I18N::translate('Botswana'),
7040
            'CAF' => /* I18N: Name of a country or state */ I18N::translate('Central African Republic'),
7041
            'CAN' => /* I18N: Name of a country or state */ I18N::translate('Canada'),
7042
            'CCK' => /* I18N: Name of a country or state */ I18N::translate('Cocos (Keeling) Islands'),
7043
            'CHE' => /* I18N: Name of a country or state */ I18N::translate('Switzerland'),
7044
            'CHL' => /* I18N: Name of a country or state */ I18N::translate('Chile'),
7045
            'CHN' => /* I18N: Name of a country or state */ I18N::translate('China'),
7046
            'CIV' => /* I18N: Name of a country or state */ I18N::translate('Cote d’Ivoire'),
7047
            'CMR' => /* I18N: Name of a country or state */ I18N::translate('Cameroon'),
7048
            'COD' => /* I18N: Name of a country or state */ I18N::translate('Democratic Republic of the Congo'),
7049
            'COG' => /* I18N: Name of a country or state */ I18N::translate('Republic of the Congo'),
7050
            'COK' => /* I18N: Name of a country or state */ I18N::translate('Cook Islands'),
7051
            'COL' => /* I18N: Name of a country or state */ I18N::translate('Colombia'),
7052
            'COM' => /* I18N: Name of a country or state */ I18N::translate('Comoros'),
7053
            'CPV' => /* I18N: Name of a country or state */ I18N::translate('Cape Verde'),
7054
            'CRI' => /* I18N: Name of a country or state */ I18N::translate('Costa Rica'),
7055
            'CUB' => /* I18N: Name of a country or state */ I18N::translate('Cuba'),
7056
            // CUW => Curaçao
7057
            'CXR' => /* I18N: Name of a country or state */ I18N::translate('Christmas Island'),
7058
            'CYM' => /* I18N: Name of a country or state */ I18N::translate('Cayman Islands'),
7059
            'CYP' => /* I18N: Name of a country or state */ I18N::translate('Cyprus'),
7060
            'CZE' => /* I18N: Name of a country or state */ I18N::translate('Czech Republic'),
7061
            'DEU' => /* I18N: Name of a country or state */ I18N::translate('Germany'),
7062
            'DJI' => /* I18N: Name of a country or state */ I18N::translate('Djibouti'),
7063
            'DMA' => /* I18N: Name of a country or state */ I18N::translate('Dominica'),
7064
            'DNK' => /* I18N: Name of a country or state */ I18N::translate('Denmark'),
7065
            'DOM' => /* I18N: Name of a country or state */ I18N::translate('Dominican Republic'),
7066
            'DZA' => /* I18N: Name of a country or state */ I18N::translate('Algeria'),
7067
            'ECU' => /* I18N: Name of a country or state */ I18N::translate('Ecuador'),
7068
            'EGY' => /* I18N: Name of a country or state */ I18N::translate('Egypt'),
7069
            'ENG' => /* I18N: Name of a country or state */ I18N::translate('England'),
7070
            'ERI' => /* I18N: Name of a country or state */ I18N::translate('Eritrea'),
7071
            'ESH' => /* I18N: Name of a country or state */ I18N::translate('Western Sahara'),
7072
            'ESP' => /* I18N: Name of a country or state */ I18N::translate('Spain'),
7073
            'EST' => /* I18N: Name of a country or state */ I18N::translate('Estonia'),
7074
            'ETH' => /* I18N: Name of a country or state */ I18N::translate('Ethiopia'),
7075
            'FIN' => /* I18N: Name of a country or state */ I18N::translate('Finland'),
7076
            'FJI' => /* I18N: Name of a country or state */ I18N::translate('Fiji'),
7077
            'FLD' => /* I18N: Name of a country or state */ I18N::translate('Flanders'),
7078
            'FLK' => /* I18N: Name of a country or state */ I18N::translate('Falkland Islands'),
7079
            'FRA' => /* I18N: Name of a country or state */ I18N::translate('France'),
7080
            'FRO' => /* I18N: Name of a country or state */ I18N::translate('Faroe Islands'),
7081
            'FSM' => /* I18N: Name of a country or state */ I18N::translate('Micronesia'),
7082
            'GAB' => /* I18N: Name of a country or state */ I18N::translate('Gabon'),
7083
            'GBR' => /* I18N: Name of a country or state */ I18N::translate('United Kingdom'),
7084
            'GEO' => /* I18N: Name of a country or state */ I18N::translate('Georgia'),
7085
            'GGY' => /* I18N: Name of a country or state */ I18N::translate('Guernsey'),
7086
            'GHA' => /* I18N: Name of a country or state */ I18N::translate('Ghana'),
7087
            'GIB' => /* I18N: Name of a country or state */ I18N::translate('Gibraltar'),
7088
            'GIN' => /* I18N: Name of a country or state */ I18N::translate('Guinea'),
7089
            'GLP' => /* I18N: Name of a country or state */ I18N::translate('Guadeloupe'),
7090
            'GMB' => /* I18N: Name of a country or state */ I18N::translate('Gambia'),
7091
            'GNB' => /* I18N: Name of a country or state */ I18N::translate('Guinea-Bissau'),
7092
            'GNQ' => /* I18N: Name of a country or state */ I18N::translate('Equatorial Guinea'),
7093
            'GRC' => /* I18N: Name of a country or state */ I18N::translate('Greece'),
7094
            'GRD' => /* I18N: Name of a country or state */ I18N::translate('Grenada'),
7095
            'GRL' => /* I18N: Name of a country or state */ I18N::translate('Greenland'),
7096
            'GTM' => /* I18N: Name of a country or state */ I18N::translate('Guatemala'),
7097
            'GUF' => /* I18N: Name of a country or state */ I18N::translate('French Guiana'),
7098
            'GUM' => /* I18N: Name of a country or state */ I18N::translate('Guam'),
7099
            'GUY' => /* I18N: Name of a country or state */ I18N::translate('Guyana'),
7100
            'HKG' => /* I18N: Name of a country or state */ I18N::translate('Hong Kong'),
7101
            'HMD' => /* I18N: Name of a country or state */ I18N::translate('Heard Island and McDonald Islands'),
7102
            'HND' => /* I18N: Name of a country or state */ I18N::translate('Honduras'),
7103
            'HRV' => /* I18N: Name of a country or state */ I18N::translate('Croatia'),
7104
            'HTI' => /* I18N: Name of a country or state */ I18N::translate('Haiti'),
7105
            'HUN' => /* I18N: Name of a country or state */ I18N::translate('Hungary'),
7106
            'IDN' => /* I18N: Name of a country or state */ I18N::translate('Indonesia'),
7107
            'IND' => /* I18N: Name of a country or state */ I18N::translate('India'),
7108
            'IOM' => /* I18N: Name of a country or state */ I18N::translate('Isle of Man'),
7109
            'IOT' => /* I18N: Name of a country or state */ I18N::translate('British Indian Ocean Territory'),
7110
            'IRL' => /* I18N: Name of a country or state */ I18N::translate('Ireland'),
7111
            'IRN' => /* I18N: Name of a country or state */ I18N::translate('Iran'),
7112
            'IRQ' => /* I18N: Name of a country or state */ I18N::translate('Iraq'),
7113
            'ISL' => /* I18N: Name of a country or state */ I18N::translate('Iceland'),
7114
            'ISR' => /* I18N: Name of a country or state */ I18N::translate('Israel'),
7115
            'ITA' => /* I18N: Name of a country or state */ I18N::translate('Italy'),
7116
            'JAM' => /* I18N: Name of a country or state */ I18N::translate('Jamaica'),
7117
            //'JEY' => Jersey
7118
            'JOR' => /* I18N: Name of a country or state */ I18N::translate('Jordan'),
7119
            'JPN' => /* I18N: Name of a country or state */ I18N::translate('Japan'),
7120
            'KAZ' => /* I18N: Name of a country or state */ I18N::translate('Kazakhstan'),
7121
            'KEN' => /* I18N: Name of a country or state */ I18N::translate('Kenya'),
7122
            'KGZ' => /* I18N: Name of a country or state */ I18N::translate('Kyrgyzstan'),
7123
            'KHM' => /* I18N: Name of a country or state */ I18N::translate('Cambodia'),
7124
            'KIR' => /* I18N: Name of a country or state */ I18N::translate('Kiribati'),
7125
            'KNA' => /* I18N: Name of a country or state */ I18N::translate('Saint Kitts and Nevis'),
7126
            'KOR' => /* I18N: Name of a country or state */ I18N::translate('Korea'),
7127
            'KWT' => /* I18N: Name of a country or state */ I18N::translate('Kuwait'),
7128
            'LAO' => /* I18N: Name of a country or state */ I18N::translate('Laos'),
7129
            'LBN' => /* I18N: Name of a country or state */ I18N::translate('Lebanon'),
7130
            'LBR' => /* I18N: Name of a country or state */ I18N::translate('Liberia'),
7131
            'LBY' => /* I18N: Name of a country or state */ I18N::translate('Libya'),
7132
            'LCA' => /* I18N: Name of a country or state */ I18N::translate('Saint Lucia'),
7133
            'LIE' => /* I18N: Name of a country or state */ I18N::translate('Liechtenstein'),
7134
            'LKA' => /* I18N: Name of a country or state */ I18N::translate('Sri Lanka'),
7135
            'LSO' => /* I18N: Name of a country or state */ I18N::translate('Lesotho'),
7136
            'LTU' => /* I18N: Name of a country or state */ I18N::translate('Lithuania'),
7137
            'LUX' => /* I18N: Name of a country or state */ I18N::translate('Luxembourg'),
7138
            'LVA' => /* I18N: Name of a country or state */ I18N::translate('Latvia'),
7139
            'MAC' => /* I18N: Name of a country or state */ I18N::translate('Macau'),
7140
            // MAF => Saint Martin
7141
            'MAR' => /* I18N: Name of a country or state */ I18N::translate('Morocco'),
7142
            'MCO' => /* I18N: Name of a country or state */ I18N::translate('Monaco'),
7143
            'MDA' => /* I18N: Name of a country or state */ I18N::translate('Moldova'),
7144
            'MDG' => /* I18N: Name of a country or state */ I18N::translate('Madagascar'),
7145
            'MDV' => /* I18N: Name of a country or state */ I18N::translate('Maldives'),
7146
            'MEX' => /* I18N: Name of a country or state */ I18N::translate('Mexico'),
7147
            'MHL' => /* I18N: Name of a country or state */ I18N::translate('Marshall Islands'),
7148
            'MKD' => /* I18N: Name of a country or state */ I18N::translate('Macedonia'),
7149
            'MLI' => /* I18N: Name of a country or state */ I18N::translate('Mali'),
7150
            'MLT' => /* I18N: Name of a country or state */ I18N::translate('Malta'),
7151
            'MMR' => /* I18N: Name of a country or state */ I18N::translate('Myanmar'),
7152
            'MNG' => /* I18N: Name of a country or state */ I18N::translate('Mongolia'),
7153
            'MNP' => /* I18N: Name of a country or state */ I18N::translate('Northern Mariana Islands'),
7154
            'MNT' => /* I18N: Name of a country or state */ I18N::translate('Montenegro'),
7155
            'MOZ' => /* I18N: Name of a country or state */ I18N::translate('Mozambique'),
7156
            'MRT' => /* I18N: Name of a country or state */ I18N::translate('Mauritania'),
7157
            'MSR' => /* I18N: Name of a country or state */ I18N::translate('Montserrat'),
7158
            'MTQ' => /* I18N: Name of a country or state */ I18N::translate('Martinique'),
7159
            'MUS' => /* I18N: Name of a country or state */ I18N::translate('Mauritius'),
7160
            'MWI' => /* I18N: Name of a country or state */ I18N::translate('Malawi'),
7161
            'MYS' => /* I18N: Name of a country or state */ I18N::translate('Malaysia'),
7162
            'MYT' => /* I18N: Name of a country or state */ I18N::translate('Mayotte'),
7163
            'NAM' => /* I18N: Name of a country or state */ I18N::translate('Namibia'),
7164
            'NCL' => /* I18N: Name of a country or state */ I18N::translate('New Caledonia'),
7165
            'NER' => /* I18N: Name of a country or state */ I18N::translate('Niger'),
7166
            'NFK' => /* I18N: Name of a country or state */ I18N::translate('Norfolk Island'),
7167
            'NGA' => /* I18N: Name of a country or state */ I18N::translate('Nigeria'),
7168
            'NIC' => /* I18N: Name of a country or state */ I18N::translate('Nicaragua'),
7169
            'NIR' => /* I18N: Name of a country or state */ I18N::translate('Northern Ireland'),
7170
            'NIU' => /* I18N: Name of a country or state */ I18N::translate('Niue'),
7171
            'NLD' => /* I18N: Name of a country or state */ I18N::translate('Netherlands'),
7172
            'NOR' => /* I18N: Name of a country or state */ I18N::translate('Norway'),
7173
            'NPL' => /* I18N: Name of a country or state */ I18N::translate('Nepal'),
7174
            'NRU' => /* I18N: Name of a country or state */ I18N::translate('Nauru'),
7175
            'NZL' => /* I18N: Name of a country or state */ I18N::translate('New Zealand'),
7176
            'OMN' => /* I18N: Name of a country or state */ I18N::translate('Oman'),
7177
            'PAK' => /* I18N: Name of a country or state */ I18N::translate('Pakistan'),
7178
            'PAN' => /* I18N: Name of a country or state */ I18N::translate('Panama'),
7179
            'PCN' => /* I18N: Name of a country or state */ I18N::translate('Pitcairn'),
7180
            'PER' => /* I18N: Name of a country or state */ I18N::translate('Peru'),
7181
            'PHL' => /* I18N: Name of a country or state */ I18N::translate('Philippines'),
7182
            'PLW' => /* I18N: Name of a country or state */ I18N::translate('Palau'),
7183
            'PNG' => /* I18N: Name of a country or state */ I18N::translate('Papua New Guinea'),
7184
            'POL' => /* I18N: Name of a country or state */ I18N::translate('Poland'),
7185
            'PRI' => /* I18N: Name of a country or state */ I18N::translate('Puerto Rico'),
7186
            'PRK' => /* I18N: Name of a country or state */ I18N::translate('North Korea'),
7187
            'PRT' => /* I18N: Name of a country or state */ I18N::translate('Portugal'),
7188
            'PRY' => /* I18N: Name of a country or state */ I18N::translate('Paraguay'),
7189
            'PSE' => /* I18N: Name of a country or state */ I18N::translate('Occupied Palestinian Territory'),
7190
            'PYF' => /* I18N: Name of a country or state */ I18N::translate('French Polynesia'),
7191
            'QAT' => /* I18N: Name of a country or state */ I18N::translate('Qatar'),
7192
            'REU' => /* I18N: Name of a country or state */ I18N::translate('Reunion'),
7193
            'ROM' => /* I18N: Name of a country or state */ I18N::translate('Romania'),
7194
            'RUS' => /* I18N: Name of a country or state */ I18N::translate('Russia'),
7195
            'RWA' => /* I18N: Name of a country or state */ I18N::translate('Rwanda'),
7196
            'SAU' => /* I18N: Name of a country or state */ I18N::translate('Saudi Arabia'),
7197
            'SCT' => /* I18N: Name of a country or state */ I18N::translate('Scotland'),
7198
            'SDN' => /* I18N: Name of a country or state */ I18N::translate('Sudan'),
7199
            'SEA' => /* I18N: Name of a country or state */ I18N::translate('At sea'),
7200
            'SEN' => /* I18N: Name of a country or state */ I18N::translate('Senegal'),
7201
            'SER' => /* I18N: Name of a country or state */ I18N::translate('Serbia'),
7202
            'SGP' => /* I18N: Name of a country or state */ I18N::translate('Singapore'),
7203
            'SGS' => /* I18N: Name of a country or state */ I18N::translate('South Georgia and the South Sandwich Islands'),
7204
            'SHN' => /* I18N: Name of a country or state */ I18N::translate('Saint Helena'),
7205
            'SJM' => /* I18N: Name of a country or state */ I18N::translate('Svalbard and Jan Mayen'),
7206
            'SLB' => /* I18N: Name of a country or state */ I18N::translate('Solomon Islands'),
7207
            'SLE' => /* I18N: Name of a country or state */ I18N::translate('Sierra Leone'),
7208
            'SLV' => /* I18N: Name of a country or state */ I18N::translate('El Salvador'),
7209
            'SMR' => /* I18N: Name of a country or state */ I18N::translate('San Marino'),
7210
            'SOM' => /* I18N: Name of a country or state */ I18N::translate('Somalia'),
7211
            'SPM' => /* I18N: Name of a country or state */ I18N::translate('Saint Pierre and Miquelon'),
7212
            'SSD' => /* I18N: Name of a country or state */ I18N::translate('South Sudan'),
7213
            'STP' => /* I18N: Name of a country or state */ I18N::translate('Sao Tome and Principe'),
7214
            'SUR' => /* I18N: Name of a country or state */ I18N::translate('Suriname'),
7215
            'SVK' => /* I18N: Name of a country or state */ I18N::translate('Slovakia'),
7216
            'SVN' => /* I18N: Name of a country or state */ I18N::translate('Slovenia'),
7217
            'SWE' => /* I18N: Name of a country or state */ I18N::translate('Sweden'),
7218
            'SWZ' => /* I18N: Name of a country or state */ I18N::translate('Swaziland'),
7219
            // SXM => Sint Maarten
7220
            'SYC' => /* I18N: Name of a country or state */ I18N::translate('Seychelles'),
7221
            'SYR' => /* I18N: Name of a country or state */ I18N::translate('Syria'),
7222
            'TCA' => /* I18N: Name of a country or state */ I18N::translate('Turks and Caicos Islands'),
7223
            'TCD' => /* I18N: Name of a country or state */ I18N::translate('Chad'),
7224
            'TGO' => /* I18N: Name of a country or state */ I18N::translate('Togo'),
7225
            'THA' => /* I18N: Name of a country or state */ I18N::translate('Thailand'),
7226
            'TJK' => /* I18N: Name of a country or state */ I18N::translate('Tajikistan'),
7227
            'TKL' => /* I18N: Name of a country or state */ I18N::translate('Tokelau'),
7228
            'TKM' => /* I18N: Name of a country or state */ I18N::translate('Turkmenistan'),
7229
            'TLS' => /* I18N: Name of a country or state */ I18N::translate('Timor-Leste'),
7230
            'TON' => /* I18N: Name of a country or state */ I18N::translate('Tonga'),
7231
            'TTO' => /* I18N: Name of a country or state */ I18N::translate('Trinidad and Tobago'),
7232
            'TUN' => /* I18N: Name of a country or state */ I18N::translate('Tunisia'),
7233
            'TUR' => /* I18N: Name of a country or state */ I18N::translate('Turkey'),
7234
            'TUV' => /* I18N: Name of a country or state */ I18N::translate('Tuvalu'),
7235
            'TWN' => /* I18N: Name of a country or state */ I18N::translate('Taiwan'),
7236
            'TZA' => /* I18N: Name of a country or state */ I18N::translate('Tanzania'),
7237
            'UGA' => /* I18N: Name of a country or state */ I18N::translate('Uganda'),
7238
            'UKR' => /* I18N: Name of a country or state */ I18N::translate('Ukraine'),
7239
            'UMI' => /* I18N: Name of a country or state */ I18N::translate('US Minor Outlying Islands'),
7240
            'URY' => /* I18N: Name of a country or state */ I18N::translate('Uruguay'),
7241
            'USA' => /* I18N: Name of a country or state */ I18N::translate('United States'),
7242
            'UZB' => /* I18N: Name of a country or state */ I18N::translate('Uzbekistan'),
7243
            'VAT' => /* I18N: Name of a country or state */ I18N::translate('Vatican City'),
7244
            'VCT' => /* I18N: Name of a country or state */ I18N::translate('Saint Vincent and the Grenadines'),
7245
            'VEN' => /* I18N: Name of a country or state */ I18N::translate('Venezuela'),
7246
            'VGB' => /* I18N: Name of a country or state */ I18N::translate('British Virgin Islands'),
7247
            'VIR' => /* I18N: Name of a country or state */ I18N::translate('US Virgin Islands'),
7248
            'VNM' => /* I18N: Name of a country or state */ I18N::translate('Vietnam'),
7249
            'VUT' => /* I18N: Name of a country or state */ I18N::translate('Vanuatu'),
7250
            'WLF' => /* I18N: Name of a country or state */ I18N::translate('Wallis and Futuna'),
7251
            'WLS' => /* I18N: Name of a country or state */ I18N::translate('Wales'),
7252
            'WSM' => /* I18N: Name of a country or state */ I18N::translate('Samoa'),
7253
            'YEM' => /* I18N: Name of a country or state */ I18N::translate('Yemen'),
7254
            'ZAF' => /* I18N: Name of a country or state */ I18N::translate('South Africa'),
7255
            'ZMB' => /* I18N: Name of a country or state */ I18N::translate('Zambia'),
7256
            'ZWE' => /* I18N: Name of a country or state */ I18N::translate('Zimbabwe'),
7257
        );
7258
    }
7259
7260
    /**
7261
     * Century name, English => 21st, Polish => XXI, etc.
7262
     *
7263
     * @param int $century
7264
     *
7265
     * @return string
7266
     */
7267
    private function centuryName($century)
7268
    {
7269
        if ($century < 0) {
7270
            return str_replace(-$century, self::centuryName(-$century), /* I18N: BCE=Before the Common Era, for Julian years < 0. See http://en.wikipedia.org/wiki/Common_Era */
0 ignored issues
show
Bug Best Practice introduced by
The method Fisharebest\Webtrees\Stats::centuryName() is not static, but was called statically. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

7270
            return str_replace(-$century, self::/** @scrutinizer ignore-call */ centuryName(-$century), /* I18N: BCE=Before the Common Era, for Julian years < 0. See http://en.wikipedia.org/wiki/Common_Era */
Loading history...
7271
                I18N::translate('%s BCE', I18N::number(-$century)));
7272
        }
7273
        // The current chart engine (Google charts) can't handle <sup></sup> markup
7274
        switch ($century) {
7275
            case 21:
7276
                return strip_tags(I18N::translateContext('CENTURY', '21st'));
7277
            case 20:
7278
                return strip_tags(I18N::translateContext('CENTURY', '20th'));
7279
            case 19:
7280
                return strip_tags(I18N::translateContext('CENTURY', '19th'));
7281
            case 18:
7282
                return strip_tags(I18N::translateContext('CENTURY', '18th'));
7283
            case 17:
7284
                return strip_tags(I18N::translateContext('CENTURY', '17th'));
7285
            case 16:
7286
                return strip_tags(I18N::translateContext('CENTURY', '16th'));
7287
            case 15:
7288
                return strip_tags(I18N::translateContext('CENTURY', '15th'));
7289
            case 14:
7290
                return strip_tags(I18N::translateContext('CENTURY', '14th'));
7291
            case 13:
7292
                return strip_tags(I18N::translateContext('CENTURY', '13th'));
7293
            case 12:
7294
                return strip_tags(I18N::translateContext('CENTURY', '12th'));
7295
            case 11:
7296
                return strip_tags(I18N::translateContext('CENTURY', '11th'));
7297
            case 10:
7298
                return strip_tags(I18N::translateContext('CENTURY', '10th'));
7299
            case 9:
7300
                return strip_tags(I18N::translateContext('CENTURY', '9th'));
7301
            case 8:
7302
                return strip_tags(I18N::translateContext('CENTURY', '8th'));
7303
            case 7:
7304
                return strip_tags(I18N::translateContext('CENTURY', '7th'));
7305
            case 6:
7306
                return strip_tags(I18N::translateContext('CENTURY', '6th'));
7307
            case 5:
7308
                return strip_tags(I18N::translateContext('CENTURY', '5th'));
7309
            case 4:
7310
                return strip_tags(I18N::translateContext('CENTURY', '4th'));
7311
            case 3:
7312
                return strip_tags(I18N::translateContext('CENTURY', '3rd'));
7313
            case 2:
7314
                return strip_tags(I18N::translateContext('CENTURY', '2nd'));
7315
            case 1:
7316
                return strip_tags(I18N::translateContext('CENTURY', '1st'));
7317
            default:
7318
                return ($century - 1) . '01-' . $century . '00';
7319
        }
7320
    }
7321
}
7322