Completed
Push — develop ( 9087a8...c9b4ef )
by Greg
16:31 queued 05:44
created

Stats::statsPlaces()   C

Complexity

Conditions 13
Paths 24

Size

Total Lines 80
Code Lines 66

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 13
eloc 66
nc 24
nop 4
dl 0
loc 80
rs 5.0618
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) 2018 webtrees development team
5
 * This program is free software: you can redistribute it and/or modify
6
 * it under the terms of the GNU General Public License as published by
7
 * the Free Software Foundation, either version 3 of the License, or
8
 * (at your option) any later version.
9
 * This program is distributed in the hope that it will be useful,
10
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12
 * GNU General Public License for more details.
13
 * You should have received a copy of the GNU General Public License
14
 * along with this program. If not, see <http://www.gnu.org/licenses/>.
15
 */
16
namespace Fisharebest\Webtrees;
17
18
use Fisharebest\Webtrees\Functions\FunctionsDate;
19
use Fisharebest\Webtrees\Functions\FunctionsDb;
20
use Fisharebest\Webtrees\Functions\FunctionsPrint;
21
use Fisharebest\Webtrees\Functions\FunctionsPrintLists;
22
use Fisharebest\Webtrees\Module\FamilyTreeFavoritesModule;
23
use Fisharebest\Webtrees\Module\UserFavoritesModule;
24
use Fisharebest\Webtrees\Query\QueryName;
25
use PDO;
26
use PDOException;
27
28
/**
29
 * A selection of pre-formatted statistical queries.
30
 *
31
 * These are primarily used for embedded keywords on HTML blocks, but
32
 * are also used elsewhere in the code.
33
 */
34
class Stats {
35
	/** @var Tree Generate statistics for a specified tree. */
36
	private $tree;
37
38
	/** @var string[] All public functions are available as keywords - except these ones */
39
	private $public_but_not_allowed = [
40
		'__construct', 'embedTags', 'iso3166', 'getAllCountries', 'getAllTagsTable', 'getAllTagsText', 'statsPlaces', 'statsBirthQuery', 'statsDeathQuery', 'statsMarrQuery', 'statsAgeQuery', 'monthFirstChildQuery', 'statsChildrenQuery', 'statsMarrAgeQuery',
41
	];
42
43
	/** @var string[] List of GEDCOM media types */
44
	private $_media_types = ['audio', 'book', 'card', 'certificate', 'coat', 'document', 'electronic', 'magazine', 'manuscript', 'map', 'fiche', 'film', 'newspaper', 'painting', 'photo', 'tombstone', 'video', 'other'];
45
46
	/**
47
	 * Create the statistics for a tree.
48
	 *
49
	 * @param Tree $tree Generate statistics for this tree
50
	 */
51
	public function __construct(Tree $tree) {
52
		$this->tree = $tree;
53
	}
54
55
	/**
56
	 * Return a string of all supported tags and an example of its output in table row form.
57
	 *
58
	 * @return string
59
	 */
60
	public function getAllTagsTable() {
61
		$examples = [];
62
		foreach (get_class_methods($this) as $method) {
63
			$reflection = new \ReflectionMethod($this, $method);
64
			if ($reflection->isPublic() && !in_array($method, $this->public_but_not_allowed)) {
65
				$examples[$method] = $this->$method();
66
			}
67
		}
68
		ksort($examples);
69
70
		$html = '';
71
		foreach ($examples as $tag => $value) {
72
			$html .= '<tr>';
73
			$html .= '<td class="list_value_wrap">' . $tag . '</td>';
74
			$html .= '<td class="list_value_wrap">' . $value . '</td>';
75
			$html .= '</tr>';
76
		}
77
78
		return
79
			'<table id="keywords" style="width:100%; table-layout:fixed"><thead>' .
80
			'<tr>' .
81
			'<th class="list_label_wrap width25">' .
82
			I18N::translate('Embedded variable') .
83
			'</th>' .
84
			'<th class="list_label_wrap width75">' .
85
			I18N::translate('Resulting value') .
86
			'</th>' .
87
			'</tr>' .
88
			'</thead><tbody>' .
89
			$html .
90
			'</tbody></table>';
91
	}
92
93
	/**
94
	 * Return a string of all supported tags in plain text.
95
	 *
96
	 * @return string
97
	 */
98
	public function getAllTagsText() {
99
		$examples = [];
100
		foreach (get_class_methods($this) as $method) {
101
			$reflection = new \ReflectionMethod($this, $method);
102
			if ($reflection->isPublic() && !in_array($method, $this->public_but_not_allowed)) {
103
				$examples[$method] = $method;
104
			}
105
		}
106
		ksort($examples);
107
108
		return implode('<br>', $examples);
109
	}
110
111
	/**
112
	 * Get tags and their parsed results.
113
	 *
114
	 * @param string $text
115
	 *
116
	 * @return string[][]
117
	 */
118
	private function getTags($text) {
119
		static $funcs;
120
121
		// Retrive all class methods
122
		isset($funcs) or $funcs = get_class_methods($this);
0 ignored issues
show
Comprehensibility Best Practice introduced by
Using logical operators such as or instead of || is generally not recommended.

PHP has two types of connecting operators (logical operators, and boolean operators):

  Logical Operators Boolean Operator
AND - meaning and &&
OR - meaning or ||

The difference between these is the order in which they are executed. In most cases, you would want to use a boolean operator like &&, or ||.

Let’s take a look at a few examples:

// Logical operators have lower precedence:
$f = false or true;

// is executed like this:
($f = false) or true;


// Boolean operators have higher precedence:
$f = false || true;

// is executed like this:
$f = (false || true);

Logical Operators are used for Control-Flow

One case where you explicitly want to use logical operators is for control-flow such as this:

$x === 5
    or die('$x must be 5.');

// Instead of
if ($x !== 5) {
    die('$x must be 5.');
}

Since die introduces problems of its own, f.e. it makes our code hardly testable, and prevents any kind of more sophisticated error handling; you probably do not want to use this in real-world code. Unfortunately, logical operators cannot be combined with throw at this point:

// The following is currently a parse error.
$x === 5
    or throw new RuntimeException('$x must be 5.');

These limitations lead to logical operators rarely being of use in current PHP code.

Loading history...
123
124
		// Extract all tags from the provided text
125
		preg_match_all('/#([^#]+)(?=#)/', (string) $text, $match);
126
		$tags       = $match[1];
127
		$c          = count($tags);
128
		$new_tags   = []; // tag to replace
129
		$new_values = []; // value to replace it with
130
131
		/*
132
		 * Parse block tags.
133
		 */
134
		for ($i = 0; $i < $c; $i++) {
135
			$full_tag = $tags[$i];
136
			// Added for new parameter support
137
			$params = explode(':', $tags[$i]);
138
			if (count($params) > 1) {
139
				$tags[$i] = array_shift($params);
140
			} else {
141
				$params = [];
142
			}
143
144
			// Generate the replacement value for the tag
145
			if (method_exists($this, $tags[$i])) {
146
				$new_tags[]   = "#{$full_tag}#";
147
				$new_values[] = call_user_func_array([$this, $tags[$i]], [$params]);
148
			}
149
		}
150
151
		return [$new_tags, $new_values];
152
	}
153
154
	/**
155
	 * Embed tags in text
156
	 *
157
	 * @param string $text
158
	 *
159
	 * @return string
160
	 */
161
	public function embedTags($text) {
162
		if (strpos($text, '#') !== false) {
163
			list($new_tags, $new_values) = $this->getTags($text);
164
			$text                        = str_replace($new_tags, $new_values, $text);
165
		}
166
167
		return $text;
168
	}
169
170
	/**
171
	 * Get the name used for GEDCOM files and URLs.
172
	 *
173
	 * @return string
174
	 */
175
	public function gedcomFilename() {
176
		return $this->tree->getName();
177
	}
178
179
	/**
180
	 * Get the internal ID number of the tree.
181
	 *
182
	 * @return int
183
	 */
184
	public function gedcomId() {
185
		return $this->tree->getTreeId();
186
	}
187
188
	/**
189
	 * Get the descriptive title of the tree.
190
	 *
191
	 * @return string
192
	 */
193
	public function gedcomTitle() {
194
		return $this->tree->getTitleHtml();
195
	}
196
197
	/**
198
	 * Get information from the GEDCOM's HEAD record.
199
	 *
200
	 * @return string[]
201
	 */
202
	private function gedcomHead() {
203
		$title   = '';
204
		$version = '';
205
		$source  = '';
206
207
		$head = GedcomRecord::getInstance('HEAD', $this->tree);
208
		$sour = $head->getFirstFact('SOUR');
209
		if ($sour !== null) {
210
			$source  = $sour->getValue();
211
			$title   = $sour->getAttribute('NAME');
212
			$version = $sour->getAttribute('VERS');
213
		}
214
215
		return [$title, $version, $source];
216
	}
217
218
	/**
219
	 * Get the software originally used to create the GEDCOM file.
220
	 *
221
	 * @return string
222
	 */
223
	public function gedcomCreatedSoftware() {
224
		$head = $this->gedcomHead();
225
226
		return $head[0];
227
	}
228
229
	/**
230
	 * Get the version of software which created the GEDCOM file.
231
	 *
232
	 * @return string
233
	 */
234
	public function gedcomCreatedVersion() {
235
		$head = $this->gedcomHead();
236
		// fix broken version string in Family Tree Maker
237
		if (strstr($head[1], 'Family Tree Maker ')) {
238
			$p       = strpos($head[1], '(') + 1;
239
			$p2      = strpos($head[1], ')');
240
			$head[1] = substr($head[1], $p, ($p2 - $p));
241
		}
242
		// Fix EasyTree version
243
		if ($head[2] == 'EasyTree') {
244
			$head[1] = substr($head[1], 1);
245
		}
246
247
		return $head[1];
248
	}
249
250
	/**
251
	 * Get the date the GEDCOM file was created.
252
	 *
253
	 * @return string
254
	 */
255
	public function gedcomDate() {
256
		$head = GedcomRecord::getInstance('HEAD', $this->tree);
257
		$fact = $head->getFirstFact('DATE');
258
		if ($fact) {
259
			$date = new Date($fact->getValue());
260
261
			return $date->display();
262
		}
263
264
		return '';
265
	}
266
267
	/**
268
	 * When was this tree last updated?
269
	 *
270
	 * @return string
271
	 */
272
	public function gedcomUpdated() {
273
		$row = Database::prepare(
274
			"SELECT SQL_CACHE 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"
275
		)->execute([$this->tree->getTreeId()])->fetchOneRow();
276
		if ($row) {
277
			$date = new Date("{$row->d_day} {$row->d_month} {$row->d_year}");
278
279
			return $date->display();
280
		} else {
281
			return $this->gedcomDate();
282
		}
283
	}
284
285
	/**
286
	 * What is the significant individual from this tree?
287
	 *
288
	 * @return string
289
	 */
290
	public function gedcomRootId() {
291
		return $this->tree->getPreference('PEDIGREE_ROOT_ID');
292
	}
293
294
	/**
295
	 * Convert totals into percentages.
296
	 *
297
	 * @param string $total
298
	 * @param string $type
299
	 *
300
	 * @return string
301
	 */
302
	private function getPercentage($total, $type) {
303
		switch ($type) {
304
			case 'individual':
305
				$type = $this->totalIndividualsQuery();
306
				break;
307
			case 'family':
308
				$type = $this->totalFamiliesQuery();
309
				break;
310
			case 'source':
311
				$type = $this->totalSourcesQuery();
312
				break;
313
			case 'note':
314
				$type = $this->totalNotesQuery();
315
				break;
316
			case 'all':
317
			default:
318
				$type = $this->totalIndividualsQuery() + $this->totalFamiliesQuery() + $this->totalSourcesQuery();
319
				break;
320
		}
321
		if ($type == 0) {
322
			return I18N::percentage(0, 1);
323
		} else {
324
			return I18N::percentage($total / $type, 1);
325
		}
326
	}
327
328
	/**
329
	 * How many GEDCOM records exist in the tree.
330
	 *
331
	 * @return string
332
	 */
333
	public function totalRecords() {
334
		return I18N::number($this->totalIndividualsQuery() + $this->totalFamiliesQuery() + $this->totalSourcesQuery());
335
	}
336
337
	/**
338
	 * How many individuals exist in the tree.
339
	 *
340
	 * @return int
341
	 */
342
	private function totalIndividualsQuery() {
343
		return (int) Database::prepare(
344
			"SELECT SQL_CACHE COUNT(*) FROM `##individuals` WHERE i_file = :tree_id"
345
		)->execute([
346
			'tree_id' => $this->tree->getTreeId(),
347
		])->fetchOne();
348
	}
349
350
	/**
351
	 * How many individuals exist in the tree.
352
	 *
353
	 * @return string
354
	 */
355
	public function totalIndividuals() {
356
		return I18N::number($this->totalIndividualsQuery());
357
	}
358
359
	/**
360
	 * How many individuals have one or more sources.
361
	 *
362
	 * @return int
363
	 */
364
	private function totalIndisWithSourcesQuery() {
365
		return (int) Database::prepare(
366
			"SELECT SQL_CACHE COUNT(DISTINCT i_id)" .
367
			" FROM `##individuals` JOIN `##link` ON i_id = l_from AND i_file = l_file" .
368
			" WHERE l_file = :tree_id AND l_type = 'SOUR'"
369
		)->execute([
370
			'tree_id' => $this->tree->getTreeId(),
371
		])->fetchOne();
372
	}
373
374
	/**
375
	 * How many individuals have one or more sources.
376
	 *
377
	 * @return string
378
	 */
379
	public function totalIndisWithSources() {
380
		return I18N::number($this->totalIndisWithSourcesQuery());
381
	}
382
383
	/**
384
	 * Create a chart showing individuals with/without sources.
385
	 *
386
	 * @param string[] $params
387
	 *
388
	 * @return string
389
	 */
390
	public function chartIndisWithSources($params = []) {
391
		$WT_STATS_CHART_COLOR1 = Theme::theme()->parameter('distribution-chart-no-values');
392
		$WT_STATS_CHART_COLOR2 = Theme::theme()->parameter('distribution-chart-high-values');
393
		$WT_STATS_S_CHART_X    = Theme::theme()->parameter('stats-small-chart-x');
394
		$WT_STATS_S_CHART_Y    = Theme::theme()->parameter('stats-small-chart-y');
395
396
		if (isset($params[0]) && $params[0] != '') {
397
			$size = strtolower($params[0]);
398
		} else {
399
			$size = $WT_STATS_S_CHART_X . 'x' . $WT_STATS_S_CHART_Y;
400
		}
401
		if (isset($params[1]) && $params[1] != '') {
402
			$color_from = strtolower($params[1]);
403
		} else {
404
			$color_from = $WT_STATS_CHART_COLOR1;
405
		}
406
		if (isset($params[2]) && $params[2] != '') {
407
			$color_to = strtolower($params[2]);
408
		} else {
409
			$color_to = $WT_STATS_CHART_COLOR2;
410
		}
411
		$sizes    = explode('x', $size);
412
		$tot_indi = $this->totalIndividualsQuery();
413
		if ($tot_indi == 0) {
414
			return '';
415
		} else {
416
			$tot_sindi_per = round($this->totalIndisWithSourcesQuery() / $tot_indi, 3);
417
			$chd           = $this->arrayToExtendedEncoding([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

417
			$chd           = $this->arrayToExtendedEncoding(/** @scrutinizer ignore-type */ [100 - 100 * $tot_sindi_per, 100 * $tot_sindi_per]);
Loading history...
418
			$chl           = I18N::translate('Without sources') . ' - ' . I18N::percentage(1 - $tot_sindi_per, 1) . '|' .
419
				I18N::translate('With sources') . ' - ' . I18N::percentage($tot_sindi_per, 1);
420
			$chart_title = I18N::translate('Individuals with sources');
421
422
			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 . '">';
423
		}
424
	}
425
426
	/**
427
	 * Show the total individuals as a percentage.
428
	 *
429
	 * @return string
430
	 */
431
	public function totalIndividualsPercentage() {
432
		return $this->getPercentage($this->totalIndividualsQuery(), 'all');
433
	}
434
435
	/**
436
	 * Count the total families.
437
	 *
438
	 * @return int
439
	 */
440
	private function totalFamiliesQuery() {
441
		return (int) Database::prepare(
442
			"SELECT SQL_CACHE COUNT(*) FROM `##families` WHERE f_file = :tree_id"
443
		)->execute([
444
			'tree_id' => $this->tree->getTreeId(),
445
		])->fetchOne();
446
	}
447
448
	/**
449
	 * Count the total families.
450
	 *
451
	 * @return string
452
	 */
453
	public function totalFamilies() {
454
		return I18N::number($this->totalFamiliesQuery());
455
	}
456
457
	/**
458
	 * Count the families with source records.
459
	 *
460
	 * @return int
461
	 */
462
	private function totalFamsWithSourcesQuery() {
463
		return (int) Database::prepare(
464
			"SELECT SQL_CACHE COUNT(DISTINCT f_id)" .
465
			" FROM `##families` JOIN `##link` ON f_id = l_from AND f_file = l_file" .
466
			" WHERE l_file = :tree_id AND l_type = 'SOUR'"
467
		)->execute([
468
			'tree_id' => $this->tree->getTreeId(),
469
		])->fetchOne();
470
	}
471
472
	/**
473
	 * Count the families with with source records.
474
	 *
475
	 * @return string
476
	 */
477
	public function totalFamsWithSources() {
478
		return I18N::number($this->totalFamsWithSourcesQuery());
479
	}
480
481
	/**
482
	 * Create a chart of individuals with/without sources.
483
	 *
484
	 * @param string[] $params
485
	 *
486
	 * @return string
487
	 */
488
	public function chartFamsWithSources($params = []) {
489
		$WT_STATS_CHART_COLOR1 = Theme::theme()->parameter('distribution-chart-no-values');
490
		$WT_STATS_CHART_COLOR2 = Theme::theme()->parameter('distribution-chart-high-values');
491
		$WT_STATS_S_CHART_X    = Theme::theme()->parameter('stats-small-chart-x');
492
		$WT_STATS_S_CHART_Y    = Theme::theme()->parameter('stats-small-chart-y');
493
494
		if (isset($params[0]) && $params[0] != '') {
495
			$size = strtolower($params[0]);
496
		} else {
497
			$size = $WT_STATS_S_CHART_X . 'x' . $WT_STATS_S_CHART_Y;
498
		}
499
		if (isset($params[1]) && $params[1] != '') {
500
			$color_from = strtolower($params[1]);
501
		} else {
502
			$color_from = $WT_STATS_CHART_COLOR1;
503
		}
504
		if (isset($params[2]) && $params[2] != '') {
505
			$color_to = strtolower($params[2]);
506
		} else {
507
			$color_to = $WT_STATS_CHART_COLOR2;
508
		}
509
		$sizes   = explode('x', $size);
510
		$tot_fam = $this->totalFamiliesQuery();
511
		if ($tot_fam == 0) {
512
			return '';
513
		} else {
514
			$tot_sfam_per = round($this->totalFamsWithSourcesQuery() / $tot_fam, 3);
515
			$chd          = $this->arrayToExtendedEncoding([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

515
			$chd          = $this->arrayToExtendedEncoding(/** @scrutinizer ignore-type */ [100 - 100 * $tot_sfam_per, 100 * $tot_sfam_per]);
Loading history...
516
			$chl          = I18N::translate('Without sources') . ' - ' . I18N::percentage(1 - $tot_sfam_per, 1) . '|' .
517
				I18N::translate('With sources') . ' - ' . I18N::percentage($tot_sfam_per, 1);
518
			$chart_title = I18N::translate('Families with sources');
519
520
			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 . '" />';
521
		}
522
	}
523
524
	/**
525
	 * Show the total families as a percentage.
526
	 *
527
	 * @return string
528
	 */
529
	public function totalFamiliesPercentage() {
530
		return $this->getPercentage($this->totalFamiliesQuery(), 'all');
531
	}
532
533
	/**
534
	 * Count the total number of sources.
535
	 *
536
	 * @return int
537
	 */
538
	private function totalSourcesQuery() {
539
		return (int) Database::prepare(
540
			"SELECT SQL_CACHE COUNT(*) FROM `##sources` WHERE s_file = :tree_id"
541
		)->execute([
542
			'tree_id' => $this->tree->getTreeId(),
543
		])->fetchOne();
544
	}
545
546
	/**
547
	 * Count the total number of sources.
548
	 *
549
	 * @return string
550
	 */
551
	public function totalSources() {
552
		return I18N::number($this->totalSourcesQuery());
553
	}
554
555
	/**
556
	 * Show the number of sources as a percentage.
557
	 *
558
	 * @return string
559
	 */
560
	public function totalSourcesPercentage() {
561
		return $this->getPercentage($this->totalSourcesQuery(), 'all');
562
	}
563
564
	/**
565
	 * Count the number of notes.
566
	 *
567
	 * @return int
568
	 */
569
	private function totalNotesQuery() {
570
		return (int) Database::prepare(
571
			"SELECT SQL_CACHE COUNT(*) FROM `##other` WHERE o_type='NOTE' AND o_file = :tree_id"
572
		)->execute([
573
			'tree_id' => $this->tree->getTreeId(),
574
		])->fetchOne();
575
	}
576
577
	/**
578
	 * Count the number of notes.
579
	 *
580
	 * @return string
581
	 */
582
	public function totalNotes() {
583
		return I18N::number($this->totalNotesQuery());
584
	}
585
586
	/**
587
	 * Show the number of notes as a percentage.
588
	 *
589
	 * @return string
590
	 */
591
	public function totalNotesPercentage() {
592
		return $this->getPercentage($this->totalNotesQuery(), 'all');
593
	}
594
595
	/**
596
	 * Count the number of repositories.
597
	 *
598
	 * @return int
599
	 */
600
	private function totalRepositoriesQuery() {
601
		return (int) Database::prepare(
602
			"SELECT SQL_CACHE COUNT(*) FROM `##other` WHERE o_type='REPO' AND o_file = :tree_id"
603
		)->execute([
604
			'tree_id' => $this->tree->getTreeId(),
605
		])->fetchOne();
606
	}
607
608
	/**
609
	 * Count the number of repositories
610
	 *
611
	 * @return string
612
	 */
613
	public function totalRepositories() {
614
		return I18N::number($this->totalRepositoriesQuery());
615
	}
616
617
	/**
618
	 * Show the total number of repositories as a percentage.
619
	 *
620
	 * @return string
621
	 */
622
	public function totalRepositoriesPercentage() {
623
		return $this->getPercentage($this->totalRepositoriesQuery(), 'all');
624
	}
625
626
	/**
627
	 * Count the surnames.
628
	 *
629
	 * @param string[] $params
630
	 *
631
	 * @return string
632
	 */
633
	public function totalSurnames($params = []) {
634
		if ($params) {
0 ignored issues
show
introduced by
The condition $params can never be true.
Loading history...
635
			$opt      = 'IN (' . implode(',', array_fill(0, count($params), '?')) . ')';
636
			$distinct = '';
637
		} else {
638
			$opt      = "IS NOT NULL";
639
			$distinct = 'DISTINCT';
640
		}
641
		$params[] = $this->tree->getTreeId();
642
		$total    =
643
			Database::prepare(
644
				"SELECT SQL_CACHE COUNT({$distinct} n_surn COLLATE '" . I18N::collation() . "')" .
645
				" FROM `##name`" .
646
				" WHERE n_surn COLLATE '" . I18N::collation() . "' {$opt} AND n_file=?"
647
			)->execute(
648
				$params
649
			)->fetchOne();
650
651
		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

651
		return I18N::number(/** @scrutinizer ignore-type */ $total);
Loading history...
652
	}
653
654
	/**
655
	 * Count the number of distinct given names, or count the number of
656
	 * occurrences of a specific name or names.
657
	 *
658
	 * @param string[] $params
659
	 *
660
	 * @return string
661
	 */
662
	public function totalGivennames($params = []) {
663
		if ($params) {
0 ignored issues
show
introduced by
The condition $params can never be true.
Loading history...
664
			$qs       = implode(',', array_fill(0, count($params), '?'));
665
			$params[] = $this->tree->getTreeId();
666
			$total    =
667
				Database::prepare("SELECT SQL_CACHE COUNT( n_givn) FROM `##name` WHERE n_givn IN ({$qs}) AND n_file=?")
668
					->execute($params)
669
					->fetchOne();
670
		} else {
671
			$total =
672
				Database::prepare("SELECT SQL_CACHE COUNT(DISTINCT n_givn) FROM `##name` WHERE n_givn IS NOT NULL AND n_file=?")
673
					->execute([$this->tree->getTreeId()])
674
					->fetchOne();
675
		}
676
677
		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

677
		return I18N::number(/** @scrutinizer ignore-type */ $total);
Loading history...
678
	}
679
680
	/**
681
	 * Count the number of events (with dates).
682
	 *
683
	 * @param string[] $params
684
	 *
685
	 * @return string
686
	 */
687
	public function totalEvents($params = []) {
688
		$sql  = "SELECT SQL_CACHE COUNT(*) AS tot FROM `##dates` WHERE d_file=?";
689
		$vars = [$this->tree->getTreeId()];
690
691
		$no_types = ['HEAD', 'CHAN'];
692
		if ($params) {
0 ignored issues
show
introduced by
The condition $params can never be true.
Loading history...
693
			$types = [];
694
			foreach ($params as $type) {
695
				if (substr($type, 0, 1) == '!') {
696
					$no_types[] = substr($type, 1);
697
				} else {
698
					$types[] = $type;
699
				}
700
			}
701
			if ($types) {
702
				$sql .= ' AND d_fact IN (' . implode(', ', array_fill(0, count($types), '?')) . ')';
703
				$vars = array_merge($vars, $types);
704
			}
705
		}
706
		$sql .= ' AND d_fact NOT IN (' . implode(', ', array_fill(0, count($no_types), '?')) . ')';
707
		$vars = array_merge($vars, $no_types);
708
709
		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

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

1400
		$chd         = $this->arrayToExtendedEncoding(/** @scrutinizer ignore-type */ $mediaCounts);
Loading history...
1401
		$chl         = substr($mediaTypes, 0, -1);
1402
1403
		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 . '" />';
1404
	}
1405
1406
	/**
1407
	 * Birth and Death
1408
	 *
1409
	 * @param string $type
1410
	 * @param string $life_dir
1411
	 * @param string $birth_death
1412
	 *
1413
	 * @return string
1414
	 */
1415
	private function mortalityQuery($type = 'full', $life_dir = 'ASC', $birth_death = 'BIRT') {
1416
		if ($birth_death == 'MARR') {
1417
			$query_field = "'MARR'";
1418
		} elseif ($birth_death == 'DIV') {
1419
			$query_field = "'DIV'";
1420
		} elseif ($birth_death == 'BIRT') {
1421
			$query_field = "'BIRT'";
1422
		} else {
1423
			$query_field = "'DEAT'";
1424
		}
1425
		if ($life_dir == 'ASC') {
1426
			$dmod = 'MIN';
1427
		} else {
1428
			$dmod = 'MAX';
1429
		}
1430
		$rows = $this->runSql(
1431
			"SELECT SQL_CACHE d_year, d_type, d_fact, d_gid" .
1432
			" FROM `##dates`" .
1433
			" WHERE d_file={$this->tree->getTreeId()} AND d_fact IN ({$query_field}) AND d_julianday1=(" .
1434
			" SELECT {$dmod}( d_julianday1 )" .
1435
			" FROM `##dates`" .
1436
			" WHERE d_file={$this->tree->getTreeId()} AND d_fact IN ({$query_field}) AND d_julianday1<>0 )" .
1437
			" LIMIT 1"
1438
		);
1439
		if (!isset($rows[0])) {
1440
			return '';
1441
		}
1442
		$row    = $rows[0];
1443
		$record = GedcomRecord::getInstance($row['d_gid'], $this->tree);
1444
		switch ($type) {
1445
			default:
1446
			case 'full':
1447
				if ($record->canShow()) {
1448
					$result = $record->formatList();
1449
				} else {
1450
					$result = I18N::translate('This information is private and cannot be shown.');
1451
				}
1452
				break;
1453
			case 'year':
1454
				if ($row['d_year'] < 0) {
1455
					$row['d_year'] = abs($row['d_year']) . ' B.C.';
1456
				}
1457
				$date   = new Date($row['d_type'] . ' ' . $row['d_year']);
1458
				$result = $date->display();
1459
				break;
1460
			case 'name':
1461
				$result = '<a href="' . e($record->url()) . '">' . $record->getFullName() . '</a>';
1462
				break;
1463
			case 'place':
1464
				$fact = GedcomRecord::getInstance($row['d_gid'], $this->tree)->getFirstFact($row['d_fact']);
1465
				if ($fact) {
1466
					$result = FunctionsPrint::formatFactPlace($fact, true, true, true);
1467
				} else {
1468
					$result = I18N::translate('Private');
1469
				}
1470
				break;
1471
		}
1472
1473
		return $result;
1474
	}
1475
1476
	/**
1477
	 * Places
1478
	 *
1479
	 * @param string $what
1480
	 * @param string $fact
1481
	 * @param int    $parent
1482
	 * @param bool   $country
1483
	 *
1484
	 * @return int[]|string[][]
1485
	 */
1486
	public function statsPlaces($what = 'ALL', $fact = '', $parent = 0, $country = false) {
1487
		if ($fact) {
1488
			if ($what == 'INDI') {
1489
				$rows = Database::prepare(
1490
					"SELECT i_gedcom AS ged FROM `##individuals` WHERE i_file = :tree_id AND i_gedcom LIKE '%\n2 PLAC %'"
1491
				)->execute([
1492
					'tree_id' => $this->tree->getTreeId(),
1493
				])->fetchAll();
1494
			} elseif ($what == 'FAM') {
1495
				$rows = Database::prepare(
1496
					"SELECT f_gedcom AS ged FROM `##families` WHERE f_file = :tree_id AND f_gedcom LIKE '%\n2 PLAC %'"
1497
				)->execute([
1498
					'tree_id' => $this->tree->getTreeId(),
1499
				])->fetchAll();
1500
			}
1501
			$placelist = [];
1502
			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...
1503
				if (preg_match('/\n1 ' . $fact . '(?:\n[2-9].*)*\n2 PLAC (.+)/', $row->ged, $match)) {
1504
					if ($country) {
1505
						$tmp   = explode(Place::GEDCOM_SEPARATOR, $match[1]);
1506
						$place = end($tmp);
1507
					} else {
1508
						$place = $match[1];
1509
					}
1510
					if (!isset($placelist[$place])) {
1511
						$placelist[$place] = 1;
1512
					} else {
1513
						$placelist[$place]++;
1514
					}
1515
				}
1516
			}
1517
1518
			return $placelist;
1519
		} elseif ($parent > 0) {
1520
			// used by placehierarchy googlemap module
1521
			if ($what == 'INDI') {
1522
				$join = " JOIN `##individuals` ON pl_file = i_file AND pl_gid = i_id";
1523
			} elseif ($what == 'FAM') {
1524
				$join = " JOIN `##families` ON pl_file = f_file AND pl_gid = f_id";
1525
			} else {
1526
				$join = "";
1527
			}
1528
			$rows = $this->runSql(
1529
				" SELECT SQL_CACHE" .
1530
				" p_place AS place," .
1531
				" COUNT(*) AS tot" .
1532
				" FROM" .
1533
				" `##places`" .
1534
				" JOIN `##placelinks` ON pl_file=p_file AND p_id=pl_p_id" .
1535
				$join .
1536
				" WHERE" .
1537
				" p_id={$parent} AND" .
1538
				" p_file={$this->tree->getTreeId()}" .
1539
				" GROUP BY place"
1540
			);
1541
1542
			return $rows;
1543
		} else {
1544
			if ($what == 'INDI') {
1545
				$join = " JOIN `##individuals` ON pl_file = i_file AND pl_gid = i_id";
1546
			} elseif ($what == 'FAM') {
1547
				$join = " JOIN `##families` ON pl_file = f_file AND pl_gid = f_id";
1548
			} else {
1549
				$join = "";
1550
			}
1551
			$rows = $this->runSql(
1552
				" SELECT SQL_CACHE" .
1553
				" p_place AS country," .
1554
				" COUNT(*) AS tot" .
1555
				" FROM" .
1556
				" `##places`" .
1557
				" JOIN `##placelinks` ON pl_file=p_file AND p_id=pl_p_id" .
1558
				$join .
1559
				" WHERE" .
1560
				" p_file={$this->tree->getTreeId()}" .
1561
				" AND p_parent_id='0'" .
1562
				" GROUP BY country ORDER BY tot DESC, country ASC"
1563
			);
1564
1565
			return $rows;
1566
		}
1567
	}
1568
1569
	/**
1570
	 * Count total places.
1571
	 *
1572
	 * @return int
1573
	 */
1574
	private function totalPlacesQuery() {
1575
		return
1576
			(int) Database::prepare("SELECT SQL_CACHE COUNT(*) FROM `##places` WHERE p_file=?")
1577
				->execute([$this->tree->getTreeId()])
1578
				->fetchOne();
1579
	}
1580
1581
	/**
1582
	 * Count total places.
1583
	 *
1584
	 * @return string
1585
	 */
1586
	public function totalPlaces() {
1587
		return I18N::number($this->totalPlacesQuery());
1588
	}
1589
1590
	/**
1591
	 * Create a chart showing where events occurred.
1592
	 *
1593
	 * @param string[] $params
1594
	 *
1595
	 * @return string
1596
	 */
1597
	public function chartDistribution($params = []) {
1598
		$WT_STATS_CHART_COLOR1 = Theme::theme()->parameter('distribution-chart-no-values');
1599
		$WT_STATS_CHART_COLOR2 = Theme::theme()->parameter('distribution-chart-high-values');
1600
		$WT_STATS_CHART_COLOR3 = Theme::theme()->parameter('distribution-chart-low-values');
1601
		$WT_STATS_MAP_X        = Theme::theme()->parameter('distribution-chart-x');
1602
		$WT_STATS_MAP_Y        = Theme::theme()->parameter('distribution-chart-y');
1603
1604
		if (isset($params[0])) {
1605
			$chart_shows = $params[0];
1606
		} else {
1607
			$chart_shows = 'world';
1608
		}
1609
		if (isset($params[1])) {
1610
			$chart_type = $params[1];
1611
		} else {
1612
			$chart_type = '';
1613
		}
1614
		if (isset($params[2])) {
1615
			$surname = $params[2];
1616
		} else {
1617
			$surname = '';
1618
		}
1619
1620
		if ($this->totalPlacesQuery() == 0) {
1621
			return '';
1622
		}
1623
		// Get the country names for each language
1624
		$country_to_iso3166 = [];
1625
		foreach (I18N::activeLocales() as $locale) {
1626
			I18N::init($locale->languageTag());
1627
			$countries = $this->getAllCountries();
1628
			foreach ($this->iso3166() as $three => $two) {
1629
				$country_to_iso3166[$three]             = $two;
1630
				$country_to_iso3166[$countries[$three]] = $two;
1631
			}
1632
		}
1633
		I18N::init(WT_LOCALE);
1634
		switch ($chart_type) {
1635
			case 'surname_distribution_chart':
1636
				if ($surname == '') {
1637
					$surname = $this->getCommonSurname();
1638
				}
1639
				$chart_title = I18N::translate('Surname distribution chart') . ': ' . $surname;
1640
				// Count how many people are events in each country
1641
				$surn_countries = [];
1642
				$indis          = QueryName::individuals($this->tree, I18N::strtoupper($surname), '', '', false, false);
1643
				foreach ($indis as $person) {
1644
					if (preg_match_all('/^2 PLAC (?:.*, *)*(.*)/m', $person->getGedcom(), $matches)) {
1645
						// webtrees uses 3 letter country codes and localised country names, but google uses 2 letter codes.
1646
						foreach ($matches[1] as $country) {
1647
							if (array_key_exists($country, $country_to_iso3166)) {
1648
								if (array_key_exists($country_to_iso3166[$country], $surn_countries)) {
1649
									$surn_countries[$country_to_iso3166[$country]]++;
1650
								} else {
1651
									$surn_countries[$country_to_iso3166[$country]] = 1;
1652
								}
1653
							}
1654
						}
1655
					}
1656
				}
1657
				break;
1658
			case 'birth_distribution_chart':
1659
				$chart_title = I18N::translate('Birth by country');
1660
				// Count how many people were born in each country
1661
				$surn_countries = [];
1662
				$b_countries    = $this->statsPlaces('INDI', 'BIRT', 0, true);
1663
				foreach ($b_countries as $place => $count) {
1664
					$country = $place;
1665
					if (array_key_exists($country, $country_to_iso3166)) {
1666
						if (!isset($surn_countries[$country_to_iso3166[$country]])) {
1667
							$surn_countries[$country_to_iso3166[$country]] = $count;
1668
						} else {
1669
							$surn_countries[$country_to_iso3166[$country]] += $count;
1670
						}
1671
					}
1672
				}
1673
				break;
1674
			case 'death_distribution_chart':
1675
				$chart_title = I18N::translate('Death by country');
1676
				// Count how many people were death in each country
1677
				$surn_countries = [];
1678
				$d_countries    = $this->statsPlaces('INDI', 'DEAT', 0, true);
1679
				foreach ($d_countries as $place => $count) {
1680
					$country = $place;
1681
					if (array_key_exists($country, $country_to_iso3166)) {
1682
						if (!isset($surn_countries[$country_to_iso3166[$country]])) {
1683
							$surn_countries[$country_to_iso3166[$country]] = $count;
1684
						} else {
1685
							$surn_countries[$country_to_iso3166[$country]] += $count;
1686
						}
1687
					}
1688
				}
1689
				break;
1690
			case 'marriage_distribution_chart':
1691
				$chart_title = I18N::translate('Marriage by country');
1692
				// Count how many families got marriage in each country
1693
				$surn_countries = [];
1694
				$m_countries    = $this->statsPlaces('FAM');
1695
				// webtrees uses 3 letter country codes and localised country names, but google uses 2 letter codes.
1696
				foreach ($m_countries as $place) {
1697
					$country = $place['country'];
1698
					if (array_key_exists($country, $country_to_iso3166)) {
1699
						if (!isset($surn_countries[$country_to_iso3166[$country]])) {
1700
							$surn_countries[$country_to_iso3166[$country]] = $place['tot'];
1701
						} else {
1702
							$surn_countries[$country_to_iso3166[$country]] += $place['tot'];
1703
						}
1704
					}
1705
				}
1706
				break;
1707
			case 'indi_distribution_chart':
1708
			default:
1709
				$chart_title = I18N::translate('Individual distribution chart');
1710
				// Count how many people have events in each country
1711
				$surn_countries = [];
1712
				$a_countries    = $this->statsPlaces('INDI');
1713
				// webtrees uses 3 letter country codes and localised country names, but google uses 2 letter codes.
1714
				foreach ($a_countries as $place) {
1715
					$country = $place['country'];
1716
					if (array_key_exists($country, $country_to_iso3166)) {
1717
						if (!isset($surn_countries[$country_to_iso3166[$country]])) {
1718
							$surn_countries[$country_to_iso3166[$country]] = $place['tot'];
1719
						} else {
1720
							$surn_countries[$country_to_iso3166[$country]] += $place['tot'];
1721
						}
1722
					}
1723
				}
1724
				break;
1725
		}
1726
		$chart_url = 'https://chart.googleapis.com/chart?cht=t&amp;chtm=' . $chart_shows;
1727
		$chart_url .= '&amp;chco=' . $WT_STATS_CHART_COLOR1 . ',' . $WT_STATS_CHART_COLOR3 . ',' . $WT_STATS_CHART_COLOR2; // country colours
1728
		$chart_url .= '&amp;chf=bg,s,ECF5FF'; // sea colour
1729
		$chart_url .= '&amp;chs=' . $WT_STATS_MAP_X . 'x' . $WT_STATS_MAP_Y;
1730
		$chart_url .= '&amp;chld=' . implode('', array_keys($surn_countries)) . '&amp;chd=s:';
1731
		foreach ($surn_countries as $count) {
1732
			$chart_url .= substr(WT_GOOGLE_CHART_ENCODING, (int) ($count / max($surn_countries) * 61), 1);
1733
		}
1734
		$chart = '<div id="google_charts" class="center">';
1735
		$chart .= '<p>' . $chart_title . '</p>';
1736
		$chart .= '<div><img src="' . $chart_url . '" alt="' . $chart_title . '" title="' . $chart_title . '" class="gchart" /><br>';
1737
		$chart .= '<table class="center"><tr>';
1738
		$chart .= '<td bgcolor="#' . $WT_STATS_CHART_COLOR2 . '" width="12"></td><td>' . I18N::translate('Highest population') . '</td>';
1739
		$chart .= '<td bgcolor="#' . $WT_STATS_CHART_COLOR3 . '" width="12"></td><td>' . I18N::translate('Lowest population') . '</td>';
1740
		$chart .= '<td bgcolor="#' . $WT_STATS_CHART_COLOR1 . '" width="12"></td><td>' . I18N::translate('Nobody at all') . '</td>';
1741
		$chart .= '</tr></table></div></div>';
1742
1743
		return $chart;
1744
	}
1745
1746
	/**
1747
	 * A list of common countries.
1748
	 *
1749
	 * @return string
1750
	 */
1751
	public function commonCountriesList() {
1752
		$countries = $this->statsPlaces();
1753
		if (empty($countries)) {
1754
			return '';
1755
		}
1756
		$top10 = [];
1757
		$i     = 1;
1758
		// Get the country names for each language
1759
		$country_names = [];
1760
		foreach (I18N::activeLocales() as $locale) {
1761
			I18N::init($locale->languageTag());
1762
			$all_countries = $this->getAllCountries();
1763
			foreach ($all_countries as $country_code => $country_name) {
1764
				$country_names[$country_name] = $country_code;
1765
			}
1766
		}
1767
		I18N::init(WT_LOCALE);
1768
		$all_db_countries = [];
1769
		foreach ($countries as $place) {
1770
			$country = trim($place['country']);
1771
			if (array_key_exists($country, $country_names)) {
1772
				if (!isset($all_db_countries[$country_names[$country]][$country])) {
1773
					$all_db_countries[$country_names[$country]][$country] = (int) $place['tot'];
1774
				} else {
1775
					$all_db_countries[$country_names[$country]][$country] += (int) $place['tot'];
1776
				}
1777
			}
1778
		}
1779
		// get all the user’s countries names
1780
		$all_countries = $this->getAllCountries();
1781
		foreach ($all_db_countries as $country_code => $country) {
1782
			$top10[] = '<li>';
1783
			foreach ($country as $country_name => $tot) {
1784
				$tmp   = new Place($country_name, $this->tree);
1785
				$place = '<a href="' . $tmp->getURL() . '" class="list_item">' . $all_countries[$country_code] . '</a>';
1786
				$top10[] .= $place . ' - ' . I18N::number($tot);
1787
			}
1788
			$top10[] .= '</li>';
1789
			if ($i++ == 10) {
1790
				break;
1791
			}
1792
		}
1793
		$top10 = implode('', $top10);
1794
1795
		return '<ul>' . $top10 . '</ul>';
1796
	}
1797
1798
	/**
1799
	 * A list of common birth places.
1800
	 *
1801
	 * @return string
1802
	 */
1803
	public function commonBirthPlacesList() {
1804
		$places = $this->statsPlaces('INDI', 'BIRT');
1805
		$top10  = [];
1806
		$i      = 1;
1807
		arsort($places);
1808
		foreach ($places as $place => $count) {
1809
			$tmp     = new Place($place, $this->tree);
1810
			$place   = '<a href="' . $tmp->getURL() . '" class="list_item">' . $tmp->getFullName() . '</a>';
1811
			$top10[] = '<li>' . $place . ' - ' . I18N::number($count) . '</li>';
0 ignored issues
show
Bug introduced by
$count 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

1811
			$top10[] = '<li>' . $place . ' - ' . I18N::number(/** @scrutinizer ignore-type */ $count) . '</li>';
Loading history...
1812
			if ($i++ == 10) {
1813
				break;
1814
			}
1815
		}
1816
		$top10 = implode('', $top10);
1817
1818
		return '<ul>' . $top10 . '</ul>';
1819
	}
1820
1821
	/**
1822
	 * A list of common death places.
1823
	 *
1824
	 * @return string
1825
	 */
1826
	public function commonDeathPlacesList() {
1827
		$places = $this->statsPlaces('INDI', 'DEAT');
1828
		$top10  = [];
1829
		$i      = 1;
1830
		arsort($places);
1831
		foreach ($places as $place => $count) {
1832
			$tmp     = new Place($place, $this->tree);
1833
			$place   = '<a href="' . $tmp->getURL() . '" class="list_item">' . $tmp->getFullName() . '</a>';
1834
			$top10[] = '<li>' . $place . ' - ' . I18N::number($count) . '</li>';
0 ignored issues
show
Bug introduced by
$count 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

1834
			$top10[] = '<li>' . $place . ' - ' . I18N::number(/** @scrutinizer ignore-type */ $count) . '</li>';
Loading history...
1835
			if ($i++ == 10) {
1836
				break;
1837
			}
1838
		}
1839
		$top10 = implode('', $top10);
1840
1841
		return '<ul>' . $top10 . '</ul>';
1842
	}
1843
1844
	/**
1845
	 * A list of common marriage places.
1846
	 *
1847
	 * @return string
1848
	 */
1849
	public function commonMarriagePlacesList() {
1850
		$places = $this->statsPlaces('FAM', 'MARR');
1851
		$top10  = [];
1852
		$i      = 1;
1853
		arsort($places);
1854
		foreach ($places as $place => $count) {
1855
			$tmp     = new Place($place, $this->tree);
1856
			$place   = '<a href="' . $tmp->getURL() . '" class="list_item">' . $tmp->getFullName() . '</a>';
1857
			$top10[] = '<li>' . $place . ' - ' . I18N::number($count) . '</li>';
0 ignored issues
show
Bug introduced by
$count 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

1857
			$top10[] = '<li>' . $place . ' - ' . I18N::number(/** @scrutinizer ignore-type */ $count) . '</li>';
Loading history...
1858
			if ($i++ == 10) {
1859
				break;
1860
			}
1861
		}
1862
		$top10 = implode('', $top10);
1863
1864
		return '<ul>' . $top10 . '</ul>';
1865
	}
1866
1867
	/**
1868
	 * Create a chart of birth places.
1869
	 *
1870
	 * @param bool     $simple
1871
	 * @param bool     $sex
1872
	 * @param int      $year1
1873
	 * @param int      $year2
1874
	 * @param string[] $params
1875
	 *
1876
	 * @return array|string
1877
	 */
1878
	public function statsBirthQuery($simple = true, $sex = false, $year1 = -1, $year2 = -1, $params = []) {
1879
		$WT_STATS_CHART_COLOR1 = Theme::theme()->parameter('distribution-chart-no-values');
1880
		$WT_STATS_CHART_COLOR2 = Theme::theme()->parameter('distribution-chart-high-values');
1881
		$WT_STATS_S_CHART_X    = Theme::theme()->parameter('stats-small-chart-x');
1882
		$WT_STATS_S_CHART_Y    = Theme::theme()->parameter('stats-small-chart-y');
1883
1884
		if ($simple) {
1885
			$sql =
1886
				"SELECT SQL_CACHE FLOOR(d_year/100+1) AS century, COUNT(*) AS total FROM `##dates` " .
1887
				"WHERE " .
1888
				"d_file = {$this->tree->getTreeId()} AND " .
1889
				"d_year <> 0 AND " .
1890
				"d_fact='BIRT' AND " .
1891
				"d_type IN ('@#DGREGORIAN@', '@#DJULIAN@')";
1892
		} elseif ($sex) {
1893
			$sql =
1894
				"SELECT SQL_CACHE d_month, i_sex, COUNT(*) AS total FROM `##dates` " .
1895
				"JOIN `##individuals` ON d_file = i_file AND d_gid = i_id " .
1896
				"WHERE " .
1897
				"d_file={$this->tree->getTreeId()} AND " .
1898
				"d_fact='BIRT' AND " .
1899
				"d_type IN ('@#DGREGORIAN@', '@#DJULIAN@')";
1900
		} else {
1901
			$sql =
1902
				"SELECT SQL_CACHE d_month, COUNT(*) AS total FROM `##dates` " .
1903
				"WHERE " .
1904
				"d_file={$this->tree->getTreeId()} AND " .
1905
				"d_fact='BIRT' AND " .
1906
				"d_type IN ('@#DGREGORIAN@', '@#DJULIAN@')";
1907
		}
1908
		if ($year1 >= 0 && $year2 >= 0) {
1909
			$sql .= " AND d_year BETWEEN '{$year1}' AND '{$year2}'";
1910
		}
1911
		if ($simple) {
1912
			$sql .= " GROUP BY century ORDER BY century";
1913
		} else {
1914
			$sql .= " GROUP BY d_month";
1915
			if ($sex) {
1916
				$sql .= ", i_sex";
1917
			}
1918
		}
1919
		$rows = $this->runSql($sql);
1920
		if ($simple) {
1921
			if (isset($params[0]) && $params[0] != '') {
1922
				$size = strtolower($params[0]);
1923
			} else {
1924
				$size = $WT_STATS_S_CHART_X . 'x' . $WT_STATS_S_CHART_Y;
1925
			}
1926
			if (isset($params[1]) && $params[1] != '') {
1927
				$color_from = strtolower($params[1]);
1928
			} else {
1929
				$color_from = $WT_STATS_CHART_COLOR1;
1930
			}
1931
			if (isset($params[2]) && $params[2] != '') {
1932
				$color_to = strtolower($params[2]);
1933
			} else {
1934
				$color_to = $WT_STATS_CHART_COLOR2;
1935
			}
1936
			$sizes = explode('x', $size);
1937
			$tot   = 0;
1938
			foreach ($rows as $values) {
1939
				$tot += $values['total'];
1940
			}
1941
			// Beware divide by zero
1942
			if ($tot == 0) {
0 ignored issues
show
introduced by
The condition $tot == 0 can never be false.
Loading history...
1943
				return '';
1944
			}
1945
			$centuries = '';
1946
			$counts    = [];
1947
			foreach ($rows as $values) {
1948
				$counts[] = round(100 * $values['total'] / $tot, 0);
1949
				$centuries .= $this->centuryName($values['century']) . ' - ' . I18N::number($values['total']) . '|';
1950
			}
1951
			$chd = $this->arrayToExtendedEncoding($counts);
1952
			$chl = rawurlencode(substr($centuries, 0, -1));
1953
1954
			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') . '" />';
1955
		} else {
1956
			return $rows;
1957
		}
1958
	}
1959
1960
	/**
1961
	 * Create a chart of death places.
1962
	 *
1963
	 * @param bool     $simple
1964
	 * @param bool     $sex
1965
	 * @param int      $year1
1966
	 * @param int      $year2
1967
	 * @param string[] $params
1968
	 *
1969
	 * @return array|string
1970
	 */
1971
	public function statsDeathQuery($simple = true, $sex = false, $year1 = -1, $year2 = -1, $params = []) {
1972
		$WT_STATS_CHART_COLOR1 = Theme::theme()->parameter('distribution-chart-no-values');
1973
		$WT_STATS_CHART_COLOR2 = Theme::theme()->parameter('distribution-chart-high-values');
1974
		$WT_STATS_S_CHART_X    = Theme::theme()->parameter('stats-small-chart-x');
1975
		$WT_STATS_S_CHART_Y    = Theme::theme()->parameter('stats-small-chart-y');
1976
1977
		if ($simple) {
1978
			$sql =
1979
				"SELECT SQL_CACHE FLOOR(d_year/100+1) AS century, COUNT(*) AS total FROM `##dates` " .
1980
				"WHERE " .
1981
				"d_file={$this->tree->getTreeId()} AND " .
1982
				'd_year<>0 AND ' .
1983
				"d_fact='DEAT' AND " .
1984
				"d_type IN ('@#DGREGORIAN@', '@#DJULIAN@')";
1985
		} elseif ($sex) {
1986
			$sql =
1987
				"SELECT SQL_CACHE d_month, i_sex, COUNT(*) AS total FROM `##dates` " .
1988
				"JOIN `##individuals` ON d_file = i_file AND d_gid = i_id " .
1989
				"WHERE " .
1990
				"d_file={$this->tree->getTreeId()} AND " .
1991
				"d_fact='DEAT' AND " .
1992
				"d_type IN ('@#DGREGORIAN@', '@#DJULIAN@')";
1993
		} else {
1994
			$sql =
1995
				"SELECT SQL_CACHE d_month, COUNT(*) AS total FROM `##dates` " .
1996
				"WHERE " .
1997
				"d_file={$this->tree->getTreeId()} AND " .
1998
				"d_fact='DEAT' AND " .
1999
				"d_type IN ('@#DGREGORIAN@', '@#DJULIAN@')";
2000
		}
2001
		if ($year1 >= 0 && $year2 >= 0) {
2002
			$sql .= " AND d_year BETWEEN '{$year1}' AND '{$year2}'";
2003
		}
2004
		if ($simple) {
2005
			$sql .= " GROUP BY century ORDER BY century";
2006
		} else {
2007
			$sql .= " GROUP BY d_month";
2008
			if ($sex) {
2009
				$sql .= ", i_sex";
2010
			}
2011
		}
2012
		$rows = $this->runSql($sql);
2013
		if ($simple) {
2014
			if (isset($params[0]) && $params[0] != '') {
2015
				$size = strtolower($params[0]);
2016
			} else {
2017
				$size = $WT_STATS_S_CHART_X . 'x' . $WT_STATS_S_CHART_Y;
2018
			}
2019
			if (isset($params[1]) && $params[1] != '') {
2020
				$color_from = strtolower($params[1]);
2021
			} else {
2022
				$color_from = $WT_STATS_CHART_COLOR1;
2023
			}
2024
			if (isset($params[2]) && $params[2] != '') {
2025
				$color_to = strtolower($params[2]);
2026
			} else {
2027
				$color_to = $WT_STATS_CHART_COLOR2;
2028
			}
2029
			$sizes = explode('x', $size);
2030
			$tot   = 0;
2031
			foreach ($rows as $values) {
2032
				$tot += $values['total'];
2033
			}
2034
			// Beware divide by zero
2035
			if ($tot == 0) {
0 ignored issues
show
introduced by
The condition $tot == 0 can never be false.
Loading history...
2036
				return '';
2037
			}
2038
			$centuries = '';
2039
			$counts    = [];
2040
			foreach ($rows as $values) {
2041
				$counts[] = round(100 * $values['total'] / $tot, 0);
2042
				$centuries .= $this->centuryName($values['century']) . ' - ' . I18N::number($values['total']) . '|';
2043
			}
2044
			$chd = $this->arrayToExtendedEncoding($counts);
2045
			$chl = rawurlencode(substr($centuries, 0, -1));
2046
2047
			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') . '" />';
2048
		}
2049
2050
		return $rows;
2051
	}
2052
2053
	/**
2054
	 * Find the earliest birth.
2055
	 *
2056
	 * @return string
2057
	 */
2058
	public function firstBirth() {
2059
		return $this->mortalityQuery('full', 'ASC', 'BIRT');
2060
	}
2061
2062
	/**
2063
	 * Find the earliest birth year.
2064
	 *
2065
	 * @return string
2066
	 */
2067
	public function firstBirthYear() {
2068
		return $this->mortalityQuery('year', 'ASC', 'BIRT');
2069
	}
2070
2071
	/**
2072
	 * Find the name of the earliest birth.
2073
	 *
2074
	 * @return string
2075
	 */
2076
	public function firstBirthName() {
2077
		return $this->mortalityQuery('name', 'ASC', 'BIRT');
2078
	}
2079
2080
	/**
2081
	 * Find the earliest birth place.
2082
	 *
2083
	 * @return string
2084
	 */
2085
	public function firstBirthPlace() {
2086
		return $this->mortalityQuery('place', 'ASC', 'BIRT');
2087
	}
2088
2089
	/**
2090
	 * Find the latest birth.
2091
	 *
2092
	 * @return string
2093
	 */
2094
	public function lastBirth() {
2095
		return $this->mortalityQuery('full', 'DESC', 'BIRT');
2096
	}
2097
2098
	/**
2099
	 * Find the latest birth year.
2100
	 *
2101
	 * @return string
2102
	 */
2103
	public function lastBirthYear() {
2104
		return $this->mortalityQuery('year', 'DESC', 'BIRT');
2105
	}
2106
2107
	/**
2108
	 * Find the latest birth name.
2109
	 *
2110
	 * @return string
2111
	 */
2112
	public function lastBirthName() {
2113
		return $this->mortalityQuery('name', 'DESC', 'BIRT');
2114
	}
2115
2116
	/**
2117
	 * Find the latest birth place.
2118
	 *
2119
	 * @return string
2120
	 */
2121
	public function lastBirthPlace() {
2122
		return $this->mortalityQuery('place', 'DESC', 'BIRT');
2123
	}
2124
2125
	/**
2126
	 * General query on births.
2127
	 *
2128
	 * @param string[] $params
2129
	 *
2130
	 * @return string
2131
	 */
2132
	public function statsBirth($params = []) {
2133
		return $this->statsBirthQuery(true, false, -1, -1, $params);
2134
	}
2135
2136
	/**
2137
	 * Find the earliest death.
2138
	 *
2139
	 * @return string
2140
	 */
2141
	public function firstDeath() {
2142
		return $this->mortalityQuery('full', 'ASC', 'DEAT');
2143
	}
2144
2145
	/**
2146
	 * Find the earliest death year.
2147
	 *
2148
	 * @return string
2149
	 */
2150
	public function firstDeathYear() {
2151
		return $this->mortalityQuery('year', 'ASC', 'DEAT');
2152
	}
2153
2154
	/**
2155
	 * Find the earliest death name.
2156
	 *
2157
	 * @return string
2158
	 */
2159
	public function firstDeathName() {
2160
		return $this->mortalityQuery('name', 'ASC', 'DEAT');
2161
	}
2162
2163
	/**
2164
	 * Find the earliest death place.
2165
	 *
2166
	 * @return string
2167
	 */
2168
	public function firstDeathPlace() {
2169
		return $this->mortalityQuery('place', 'ASC', 'DEAT');
2170
	}
2171
2172
	/**
2173
	 * Find the latest death.
2174
	 *
2175
	 * @return string
2176
	 */
2177
	public function lastDeath() {
2178
		return $this->mortalityQuery('full', 'DESC', 'DEAT');
2179
	}
2180
2181
	/**
2182
	 * Find the latest death year.
2183
	 *
2184
	 * @return string
2185
	 */
2186
	public function lastDeathYear() {
2187
		return $this->mortalityQuery('year', 'DESC', 'DEAT');
2188
	}
2189
2190
	/**
2191
	 * Find the latest death name.
2192
	 *
2193
	 * @return string
2194
	 */
2195
	public function lastDeathName() {
2196
		return $this->mortalityQuery('name', 'DESC', 'DEAT');
2197
	}
2198
2199
	/**
2200
	 * Find the place of the latest death.
2201
	 *
2202
	 * @return string
2203
	 */
2204
	public function lastDeathPlace() {
2205
		return $this->mortalityQuery('place', 'DESC', 'DEAT');
2206
	}
2207
2208
	/**
2209
	 * General query on deaths.
2210
	 *
2211
	 * @param string[] $params
2212
	 *
2213
	 * @return string
2214
	 */
2215
	public function statsDeath($params = []) {
2216
		return $this->statsDeathQuery(true, false, -1, -1, $params);
2217
	}
2218
2219
	/**
2220
	 * Lifespan
2221
	 *
2222
	 * @param string $type
2223
	 * @param string $sex
2224
	 *
2225
	 * @return string
2226
	 */
2227
	private function longlifeQuery($type = 'full', $sex = 'F') {
2228
		$sex_search = ' 1=1';
2229
		if ($sex == 'F') {
2230
			$sex_search = " i_sex='F'";
2231
		} elseif ($sex == 'M') {
2232
			$sex_search = " i_sex='M'";
2233
		}
2234
2235
		$rows = $this->runSql(
2236
			" SELECT SQL_CACHE" .
2237
			" death.d_gid AS id," .
2238
			" death.d_julianday2-birth.d_julianday1 AS age" .
2239
			" FROM" .
2240
			" `##dates` AS death," .
2241
			" `##dates` AS birth," .
2242
			" `##individuals` AS indi" .
2243
			" WHERE" .
2244
			" indi.i_id=birth.d_gid AND" .
2245
			" birth.d_gid=death.d_gid AND" .
2246
			" death.d_file={$this->tree->getTreeId()} AND" .
2247
			" birth.d_file=death.d_file AND" .
2248
			" birth.d_file=indi.i_file AND" .
2249
			" birth.d_fact='BIRT' AND" .
2250
			" death.d_fact='DEAT' AND" .
2251
			" birth.d_julianday1<>0 AND" .
2252
			" death.d_julianday1>birth.d_julianday2 AND" .
2253
			$sex_search .
2254
			" ORDER BY" .
2255
			" age DESC LIMIT 1"
2256
		);
2257
		if (!isset($rows[0])) {
2258
			return '';
2259
		}
2260
		$row    = $rows[0];
2261
		$person = Individual::getInstance($row['id'], $this->tree);
2262
		switch ($type) {
2263
			default:
2264
			case 'full':
2265
				if ($person->canShowName()) {
2266
					$result = $person->formatList();
2267
				} else {
2268
					$result = I18N::translate('This information is private and cannot be shown.');
2269
				}
2270
				break;
2271
			case 'age':
2272
				$result = I18N::number((int) ($row['age'] / 365.25));
2273
				break;
2274
			case 'name':
2275
				$result = '<a href="' . e($person->url()) . '">' . $person->getFullName() . '</a>';
2276
				break;
2277
		}
2278
2279
		return $result;
2280
	}
2281
2282
	/**
2283
	 * Find the oldest individuals.
2284
	 *
2285
	 * @param string   $type
2286
	 * @param string   $sex
2287
	 * @param string[] $params
2288
	 *
2289
	 * @return string
2290
	 */
2291
	private function topTenOldestQuery($type = 'list', $sex = 'BOTH', $params = []) {
2292
		if ($sex === 'F') {
2293
			$sex_search = " AND i_sex='F' ";
2294
		} elseif ($sex === 'M') {
2295
			$sex_search = " AND i_sex='M' ";
2296
		} else {
2297
			$sex_search = '';
2298
		}
2299
		if (isset($params[0])) {
2300
			$total = (int) $params[0];
2301
		} else {
2302
			$total = 10;
2303
		}
2304
		$rows = $this->runSql(
2305
			"SELECT SQL_CACHE " .
2306
			" MAX(death.d_julianday2-birth.d_julianday1) AS age, " .
2307
			" death.d_gid AS deathdate " .
2308
			"FROM " .
2309
			" `##dates` AS death, " .
2310
			" `##dates` AS birth, " .
2311
			" `##individuals` AS indi " .
2312
			"WHERE " .
2313
			" indi.i_id=birth.d_gid AND " .
2314
			" birth.d_gid=death.d_gid AND " .
2315
			" death.d_file={$this->tree->getTreeId()} AND " .
2316
			" birth.d_file=death.d_file AND " .
2317
			" birth.d_file=indi.i_file AND " .
2318
			" birth.d_fact='BIRT' AND " .
2319
			" death.d_fact='DEAT' AND " .
2320
			" birth.d_julianday1<>0 AND " .
2321
			" death.d_julianday1>birth.d_julianday2 " .
2322
			$sex_search .
2323
			"GROUP BY deathdate " .
2324
			"ORDER BY age DESC " .
2325
			"LIMIT " . $total
2326
		);
2327
		if (!isset($rows[0])) {
2328
			return '';
2329
		}
2330
		$top10 = [];
2331
		foreach ($rows as $row) {
2332
			$person = Individual::getInstance($row['deathdate'], $this->tree);
2333
			$age    = $row['age'];
2334
			if ((int) ($age / 365.25) > 0) {
2335
				$age = (int) ($age / 365.25) . 'y';
2336
			} elseif ((int) ($age / 30.4375) > 0) {
2337
				$age = (int) ($age / 30.4375) . 'm';
2338
			} else {
2339
				$age = $age . 'd';
2340
			}
2341
			$age = FunctionsDate::getAgeAtEvent($age);
2342
			if ($person->canShow()) {
2343
				if ($type == 'list') {
2344
					$top10[] = '<li><a href="' . e($person->url()) . '">' . $person->getFullName() . '</a> (' . $age . ')' . '</li>';
2345
				} else {
2346
					$top10[] = '<a href="' . e($person->url()) . '">' . $person->getFullName() . '</a> (' . $age . ')';
2347
				}
2348
			}
2349
		}
2350
		if ($type == 'list') {
2351
			$top10 = implode('', $top10);
2352
		} else {
2353
			$top10 = implode(' ', $top10);
2354
		}
2355
		if (I18N::direction() === 'rtl') {
2356
			$top10 = str_replace(['[', ']', '(', ')', '+'], ['&rlm;[', '&rlm;]', '&rlm;(', '&rlm;)', '&rlm;+'], $top10);
2357
		}
2358
		if ($type == 'list') {
2359
			return '<ul>' . $top10 . '</ul>';
2360
		}
2361
2362
		return $top10;
2363
	}
2364
2365
	/**
2366
	 * Find the oldest living individuals.
2367
	 *
2368
	 * @param string   $type
2369
	 * @param string   $sex
2370
	 * @param string[] $params
2371
	 *
2372
	 * @return string
2373
	 */
2374
	private function topTenOldestAliveQuery($type = 'list', $sex = 'BOTH', $params = []) {
2375
		if (!Auth::isMember($this->tree)) {
2376
			return I18N::translate('This information is private and cannot be shown.');
2377
		}
2378
		if ($sex == 'F') {
2379
			$sex_search = " AND i_sex='F'";
2380
		} elseif ($sex == 'M') {
2381
			$sex_search = " AND i_sex='M'";
2382
		} else {
2383
			$sex_search = '';
2384
		}
2385
		if (isset($params[0])) {
2386
			$total = (int) $params[0];
2387
		} else {
2388
			$total = 10;
2389
		}
2390
		$rows = $this->runSql(
2391
			"SELECT SQL_CACHE" .
2392
			" birth.d_gid AS id," .
2393
			" MIN(birth.d_julianday1) AS age" .
2394
			" FROM" .
2395
			" `##dates` AS birth," .
2396
			" `##individuals` AS indi" .
2397
			" WHERE" .
2398
			" indi.i_id=birth.d_gid AND" .
2399
			" indi.i_gedcom NOT REGEXP '\\n1 (" . WT_EVENTS_DEAT . ")' AND" .
2400
			" birth.d_file={$this->tree->getTreeId()} AND" .
2401
			" birth.d_fact='BIRT' AND" .
2402
			" birth.d_file=indi.i_file AND" .
2403
			" birth.d_julianday1<>0" .
2404
			$sex_search .
2405
			" GROUP BY id" .
2406
			" ORDER BY age" .
2407
			" ASC LIMIT " . $total
2408
		);
2409
		$top10 = [];
2410
		foreach ($rows as $row) {
2411
			$person = Individual::getInstance($row['id'], $this->tree);
2412
			$age    = (WT_CLIENT_JD - $row['age']);
2413
			if ((int) ($age / 365.25) > 0) {
2414
				$age = (int) ($age / 365.25) . 'y';
2415
			} elseif ((int) ($age / 30.4375) > 0) {
2416
				$age = (int) ($age / 30.4375) . 'm';
2417
			} else {
2418
				$age = $age . 'd';
2419
			}
2420
			$age = FunctionsDate::getAgeAtEvent($age);
2421
			if ($type === 'list') {
2422
				$top10[] = '<li><a href="' . e($person->url()) . '">' . $person->getFullName() . '</a> (' . $age . ')' . '</li>';
2423
			} else {
2424
				$top10[] = '<a href="' . e($person->url()) . '">' . $person->getFullName() . '</a> (' . $age . ')';
2425
			}
2426
		}
2427
		if ($type === 'list') {
2428
			$top10 = implode('', $top10);
2429
		} else {
2430
			$top10 = implode('; ', $top10);
2431
		}
2432
		if (I18N::direction() === 'rtl') {
2433
			$top10 = str_replace(['[', ']', '(', ')', '+'], ['&rlm;[', '&rlm;]', '&rlm;(', '&rlm;)', '&rlm;+'], $top10);
2434
		}
2435
		if ($type === 'list') {
2436
			return '<ul>' . $top10 . '</ul>';
2437
		}
2438
2439
		return $top10;
2440
	}
2441
2442
	/**
2443
	 * Find the average lifespan.
2444
	 *
2445
	 * @param string $sex
2446
	 * @param bool   $show_years
2447
	 *
2448
	 * @return string
2449
	 */
2450
	private function averageLifespanQuery($sex = 'BOTH', $show_years = false) {
2451
		if ($sex === 'F') {
2452
			$sex_search = " AND i_sex='F' ";
2453
		} elseif ($sex === 'M') {
2454
			$sex_search = " AND i_sex='M' ";
2455
		} else {
2456
			$sex_search = '';
2457
		}
2458
		$rows = $this->runSql(
2459
			"SELECT SQL_CACHE " .
2460
			" AVG(death.d_julianday2-birth.d_julianday1) AS age " .
2461
			"FROM " .
2462
			" `##dates` AS death, " .
2463
			" `##dates` AS birth, " .
2464
			" `##individuals` AS indi " .
2465
			"WHERE " .
2466
			" indi.i_id=birth.d_gid AND " .
2467
			" birth.d_gid=death.d_gid AND " .
2468
			" death.d_file=" . $this->tree->getTreeId() . " AND " .
2469
			" birth.d_file=death.d_file AND " .
2470
			" birth.d_file=indi.i_file AND " .
2471
			" birth.d_fact='BIRT' AND " .
2472
			" death.d_fact='DEAT' AND " .
2473
			" birth.d_julianday1<>0 AND " .
2474
			" death.d_julianday1>birth.d_julianday2 " .
2475
			$sex_search
2476
		);
2477
		if (!isset($rows[0])) {
2478
			return '';
2479
		}
2480
		$row = $rows[0];
2481
		$age = $row['age'];
2482
		if ($show_years) {
2483
			if ((int) ($age / 365.25) > 0) {
2484
				$age = (int) ($age / 365.25) . 'y';
2485
			} elseif ((int) ($age / 30.4375) > 0) {
2486
				$age = (int) ($age / 30.4375) . 'm';
2487
			} elseif (!empty($age)) {
2488
				$age = $age . 'd';
2489
			}
2490
2491
			return FunctionsDate::getAgeAtEvent($age);
2492
		} else {
2493
			return I18N::number($age / 365.25);
2494
		}
2495
	}
2496
2497
	/**
2498
	 * General query on ages.
2499
	 *
2500
	 * @param bool     $simple
2501
	 * @param string   $related
2502
	 * @param string   $sex
2503
	 * @param int      $year1
2504
	 * @param int      $year2
2505
	 * @param string[] $params
2506
	 *
2507
	 * @return array|string
2508
	 */
2509
	public function statsAgeQuery($simple = true, $related = 'BIRT', $sex = 'BOTH', $year1 = -1, $year2 = -1, $params = []) {
2510
		if ($simple) {
2511
			if (isset($params[0]) && $params[0] != '') {
2512
				$size = strtolower($params[0]);
2513
			} else {
2514
				$size = '230x250';
2515
			}
2516
			$sizes = explode('x', $size);
2517
			$rows  = $this->runSql(
2518
				"SELECT SQL_CACHE" .
2519
				" ROUND(AVG(death.d_julianday2-birth.d_julianday1)/365.25,1) AS age," .
2520
				" FLOOR(death.d_year/100+1) AS century," .
2521
				" i_sex AS sex" .
2522
				" FROM" .
2523
				" `##dates` AS death," .
2524
				" `##dates` AS birth," .
2525
				" `##individuals` AS indi" .
2526
				" WHERE" .
2527
				" indi.i_id=birth.d_gid AND" .
2528
				" birth.d_gid=death.d_gid AND" .
2529
				" death.d_file={$this->tree->getTreeId()} AND" .
2530
				" birth.d_file=death.d_file AND" .
2531
				" birth.d_file=indi.i_file AND" .
2532
				" birth.d_fact='BIRT' AND" .
2533
				" death.d_fact='DEAT' AND" .
2534
				" birth.d_julianday1<>0 AND" .
2535
				" birth.d_type IN ('@#DGREGORIAN@', '@#DJULIAN@') AND" .
2536
				" death.d_type IN ('@#DGREGORIAN@', '@#DJULIAN@') AND" .
2537
				" death.d_julianday1>birth.d_julianday2" .
2538
				" GROUP BY century, sex ORDER BY century, sex");
2539
			if (empty($rows)) {
2540
				return '';
2541
			}
2542
			$chxl    = '0:|';
2543
			$countsm = '';
2544
			$countsf = '';
2545
			$countsa = '';
2546
			$out     = [];
2547
			foreach ($rows as $values) {
2548
				$out[$values['century']][$values['sex']] = $values['age'];
2549
			}
2550
			foreach ($out as $century => $values) {
2551
				if ($sizes[0] < 980) {
2552
					$sizes[0] += 50;
2553
				}
2554
				$chxl .= $this->centuryName($century) . '|';
2555
				$average = 0;
2556
				if (isset($values['F'])) {
2557
					$countsf .= $values['F'] . ',';
2558
					$average = $values['F'];
2559
				} else {
2560
					$countsf .= '0,';
2561
				}
2562
				if (isset($values['M'])) {
2563
					$countsm .= $values['M'] . ',';
2564
					if ($average == 0) {
2565
						$countsa .= $values['M'] . ',';
2566
					} else {
2567
						$countsa .= (($values['M'] + $average) / 2) . ',';
2568
					}
2569
				} else {
2570
					$countsm .= '0,';
2571
					if ($average == 0) {
2572
						$countsa .= '0,';
2573
					} else {
2574
						$countsa .= $values['F'] . ',';
2575
					}
2576
				}
2577
			}
2578
			$countsm = substr($countsm, 0, -1);
2579
			$countsf = substr($countsf, 0, -1);
2580
			$countsa = substr($countsa, 0, -1);
2581
			$chd     = 't2:' . $countsm . '|' . $countsf . '|' . $countsa;
2582
			$decades = '';
2583
			for ($i = 0; $i <= 100; $i += 10) {
2584
				$decades .= '|' . I18N::number($i);
2585
			}
2586
			$chxl .= '1:||' . I18N::translate('century') . '|2:' . $decades . '|3:||' . I18N::translate('Age') . '|';
2587
			$title = I18N::translate('Average age related to death century');
2588
			if (count($rows) > 6 || mb_strlen($title) < 30) {
2589
				$chtt = $title;
2590
			} else {
2591
				$offset  = 0;
2592
				$counter = [];
2593
				while ($offset = strpos($title, ' ', $offset + 1)) {
2594
					$counter[] = $offset;
2595
				}
2596
				$half = (int) (count($counter) / 2);
2597
				$chtt = substr_replace($title, '|', $counter[$half], 1);
2598
			}
2599
2600
			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') . '" />';
2601
		} else {
2602
			$sex_search = '';
2603
			$years      = '';
2604
			if ($sex == 'F') {
2605
				$sex_search = " AND i_sex='F'";
2606
			} elseif ($sex == 'M') {
2607
				$sex_search = " AND i_sex='M'";
2608
			}
2609
			if ($year1 >= 0 && $year2 >= 0) {
2610
				if ($related == 'BIRT') {
2611
					$years = " AND birth.d_year BETWEEN '{$year1}' AND '{$year2}'";
2612
				} elseif ($related == 'DEAT') {
2613
					$years = " AND death.d_year BETWEEN '{$year1}' AND '{$year2}'";
2614
				}
2615
			}
2616
			$rows = $this->runSql(
2617
				"SELECT SQL_CACHE" .
2618
				" death.d_julianday2-birth.d_julianday1 AS age" .
2619
				" FROM" .
2620
				" `##dates` AS death," .
2621
				" `##dates` AS birth," .
2622
				" `##individuals` AS indi" .
2623
				" WHERE" .
2624
				" indi.i_id=birth.d_gid AND" .
2625
				" birth.d_gid=death.d_gid AND" .
2626
				" death.d_file={$this->tree->getTreeId()} AND" .
2627
				" birth.d_file=death.d_file AND" .
2628
				" birth.d_file=indi.i_file AND" .
2629
				" birth.d_fact='BIRT' AND" .
2630
				" death.d_fact='DEAT' AND" .
2631
				" birth.d_julianday1 <> 0 AND" .
2632
				" birth.d_type IN ('@#DGREGORIAN@', '@#DJULIAN@') AND" .
2633
				" death.d_type IN ('@#DGREGORIAN@', '@#DJULIAN@') AND" .
2634
				" death.d_julianday1>birth.d_julianday2" .
2635
				$years .
2636
				$sex_search .
2637
				" ORDER BY age DESC");
2638
2639
			return $rows;
2640
		}
2641
	}
2642
2643
	/**
2644
	 * General query on ages.
2645
	 *
2646
	 * @param string[] $params
2647
	 *
2648
	 * @return string
2649
	 */
2650
	public function statsAge($params = []) {
2651
		return $this->statsAgeQuery(true, 'BIRT', 'BOTH', -1, -1, $params);
2652
	}
2653
2654
	/**
2655
	 * Find the lognest lived individual.
2656
	 *
2657
	 * @return string
2658
	 */
2659
	public function longestLife() {
2660
		return $this->longlifeQuery('full', 'BOTH');
2661
	}
2662
2663
	/**
2664
	 * Find the age of the longest lived individual.
2665
	 *
2666
	 * @return string
2667
	 */
2668
	public function longestLifeAge() {
2669
		return $this->longlifeQuery('age', 'BOTH');
2670
	}
2671
2672
	/**
2673
	 * Find the name of the longest lived individual.
2674
	 *
2675
	 * @return string
2676
	 */
2677
	public function longestLifeName() {
2678
		return $this->longlifeQuery('name', 'BOTH');
2679
	}
2680
2681
	/**
2682
	 * Find the oldest individuals.
2683
	 *
2684
	 * @param string[] $params
2685
	 *
2686
	 * @return string
2687
	 */
2688
	public function topTenOldest($params = []) {
2689
		return $this->topTenOldestQuery('nolist', 'BOTH', $params);
2690
	}
2691
2692
	/**
2693
	 * Find the oldest living individuals.
2694
	 *
2695
	 * @param string[] $params
2696
	 *
2697
	 * @return string
2698
	 */
2699
	public function topTenOldestList($params = []) {
2700
		return $this->topTenOldestQuery('list', 'BOTH', $params);
2701
	}
2702
2703
	/**
2704
	 * Find the oldest living individuals.
2705
	 *
2706
	 * @param string[] $params
2707
	 *
2708
	 * @return string
2709
	 */
2710
	public function topTenOldestAlive($params = []) {
2711
		return $this->topTenOldestAliveQuery('nolist', 'BOTH', $params);
2712
	}
2713
2714
	/**
2715
	 * Find the oldest living individuals.
2716
	 *
2717
	 * @param string[] $params
2718
	 *
2719
	 * @return string
2720
	 */
2721
	public function topTenOldestListAlive($params = []) {
2722
		return $this->topTenOldestAliveQuery('list', 'BOTH', $params);
2723
	}
2724
2725
	/**
2726
	 * Find the average lifespan.
2727
	 *
2728
	 * @param bool $show_years
2729
	 *
2730
	 * @return string
2731
	 */
2732
	public function averageLifespan($show_years = false) {
2733
		return $this->averageLifespanQuery('BOTH', $show_years);
2734
	}
2735
2736
	/**
2737
	 * Find the longest lived female.
2738
	 *
2739
	 * @return string
2740
	 */
2741
	public function longestLifeFemale() {
2742
		return $this->longlifeQuery('full', 'F');
2743
	}
2744
2745
	/**
2746
	 * Find the age of the longest lived female.
2747
	 *
2748
	 * @return string
2749
	 */
2750
	public function longestLifeFemaleAge() {
2751
		return $this->longlifeQuery('age', 'F');
2752
	}
2753
2754
	/**
2755
	 * Find the name of the longest lived female.
2756
	 *
2757
	 * @return string
2758
	 */
2759
	public function longestLifeFemaleName() {
2760
		return $this->longlifeQuery('name', 'F');
2761
	}
2762
2763
	/**
2764
	 * Find the oldest females.
2765
	 *
2766
	 * @param string[] $params
2767
	 *
2768
	 * @return string
2769
	 */
2770
	public function topTenOldestFemale($params = []) {
2771
		return $this->topTenOldestQuery('nolist', 'F', $params);
2772
	}
2773
2774
	/**
2775
	 * Find the oldest living females.
2776
	 *
2777
	 * @param string[] $params
2778
	 *
2779
	 * @return string
2780
	 */
2781
	public function topTenOldestFemaleList($params = []) {
2782
		return $this->topTenOldestQuery('list', 'F', $params);
2783
	}
2784
2785
	/**
2786
	 * Find the oldest living females.
2787
	 *
2788
	 * @param string[] $params
2789
	 *
2790
	 * @return string
2791
	 */
2792
	public function topTenOldestFemaleAlive($params = []) {
2793
		return $this->topTenOldestAliveQuery('nolist', 'F', $params);
2794
	}
2795
2796
	/**
2797
	 * Find the oldest living females.
2798
	 *
2799
	 * @param string[] $params
2800
	 *
2801
	 * @return string
2802
	 */
2803
	public function topTenOldestFemaleListAlive($params = []) {
2804
		return $this->topTenOldestAliveQuery('list', 'F', $params);
2805
	}
2806
2807
	/**
2808
	 * Find the average lifespan of females.
2809
	 *
2810
	 * @param bool $show_years
2811
	 *
2812
	 * @return string
2813
	 */
2814
	public function averageLifespanFemale($show_years = false) {
2815
		return $this->averageLifespanQuery('F', $show_years);
2816
	}
2817
2818
	/**
2819
	 * Find the longest lived male.
2820
	 *
2821
	 * @return string
2822
	 */
2823
	public function longestLifeMale() {
2824
		return $this->longlifeQuery('full', 'M');
2825
	}
2826
2827
	/**
2828
	 * Find the age of the longest lived male.
2829
	 *
2830
	 * @return string
2831
	 */
2832
	public function longestLifeMaleAge() {
2833
		return $this->longlifeQuery('age', 'M');
2834
	}
2835
2836
	/**
2837
	 * Find the name of the longest lived male.
2838
	 *
2839
	 * @return string
2840
	 */
2841
	public function longestLifeMaleName() {
2842
		return $this->longlifeQuery('name', 'M');
2843
	}
2844
2845
	/**
2846
	 * Find the longest lived males.
2847
	 *
2848
	 * @param string[] $params
2849
	 *
2850
	 * @return string
2851
	 */
2852
	public function topTenOldestMale($params = []) {
2853
		return $this->topTenOldestQuery('nolist', 'M', $params);
2854
	}
2855
2856
	/**
2857
	 * Find the longest lived males.
2858
	 *
2859
	 * @param string[] $params
2860
	 *
2861
	 * @return string
2862
	 */
2863
	public function topTenOldestMaleList($params = []) {
2864
		return $this->topTenOldestQuery('list', 'M', $params);
2865
	}
2866
2867
	/**
2868
	 * Find the longest lived living males.
2869
	 *
2870
	 * @param string[] $params
2871
	 *
2872
	 * @return string
2873
	 */
2874
	public function topTenOldestMaleAlive($params = []) {
2875
		return $this->topTenOldestAliveQuery('nolist', 'M', $params);
2876
	}
2877
2878
	/**
2879
	 * Find the longest lived living males.
2880
	 *
2881
	 * @param string[] $params
2882
	 *
2883
	 * @return string
2884
	 */
2885
	public function topTenOldestMaleListAlive($params = []) {
2886
		return $this->topTenOldestAliveQuery('list', 'M', $params);
2887
	}
2888
2889
	/**
2890
	 * Find the average male lifespan.
2891
	 *
2892
	 * @param bool $show_years
2893
	 *
2894
	 * @return string
2895
	 */
2896
	public function averageLifespanMale($show_years = false) {
2897
		return $this->averageLifespanQuery('M', $show_years);
2898
	}
2899
2900
	/**
2901
	 * Events
2902
	 *
2903
	 * @param string $type
2904
	 * @param string $direction
2905
	 * @param string $facts
2906
	 *
2907
	 * @return string
2908
	 */
2909
	private function eventQuery($type, $direction, $facts) {
2910
		$eventTypes = [
2911
			'BIRT' => I18N::translate('birth'),
2912
			'DEAT' => I18N::translate('death'),
2913
			'MARR' => I18N::translate('marriage'),
2914
			'ADOP' => I18N::translate('adoption'),
2915
			'BURI' => I18N::translate('burial'),
2916
			'CENS' => I18N::translate('census added'),
2917
		];
2918
2919
		$fact_query = "IN ('" . str_replace('|', "','", $facts) . "')";
2920
2921
		if ($direction != 'ASC') {
2922
			$direction = 'DESC';
2923
		}
2924
		$rows = $this->runSql(''
2925
			. ' SELECT SQL_CACHE'
2926
			. ' d_gid AS id,'
2927
			. ' d_year AS year,'
2928
			. ' d_fact AS fact,'
2929
			. ' d_type AS type'
2930
			. ' FROM'
2931
			. " `##dates`"
2932
			. ' WHERE'
2933
			. " d_file={$this->tree->getTreeId()} AND"
2934
			. " d_gid<>'HEAD' AND"
2935
			. " d_fact {$fact_query} AND"
2936
			. ' d_julianday1<>0'
2937
			. ' ORDER BY'
2938
			. " d_julianday1 {$direction}, d_type LIMIT 1"
2939
		);
2940
		if (!isset($rows[0])) {
2941
			return '';
2942
		}
2943
		$row    = $rows[0];
2944
		$record = GedcomRecord::getInstance($row['id'], $this->tree);
2945
		switch ($type) {
2946
			default:
2947
			case 'full':
2948
				if ($record->canShow()) {
2949
					$result = $record->formatList();
2950
				} else {
2951
					$result = I18N::translate('This information is private and cannot be shown.');
2952
				}
2953
				break;
2954
			case 'year':
2955
				$date   = new Date($row['type'] . ' ' . $row['year']);
2956
				$result = $date->display();
2957
				break;
2958
			case 'type':
2959
				if (isset($eventTypes[$row['fact']])) {
2960
					$result = $eventTypes[$row['fact']];
2961
				} else {
2962
					$result = GedcomTag::getLabel($row['fact']);
2963
				}
2964
				break;
2965
			case 'name':
2966
				$result = '<a href="' . e($record->url()) . '">' . $record->getFullName() . '</a>';
2967
				break;
2968
			case 'place':
2969
				$fact = $record->getFirstFact($row['fact']);
2970
				if ($fact) {
2971
					$result = FunctionsPrint::formatFactPlace($fact, true, true, true);
2972
				} else {
2973
					$result = I18N::translate('Private');
2974
				}
2975
				break;
2976
		}
2977
2978
		return $result;
2979
	}
2980
2981
	/**
2982
	 * Find the earliest event.
2983
	 *
2984
	 * @return string
2985
	 */
2986
	public function firstEvent() {
2987
		return $this->eventQuery('full', 'ASC', WT_EVENTS_BIRT . '|' . WT_EVENTS_MARR . '|' . WT_EVENTS_DIV . '|' . WT_EVENTS_DEAT);
2988
	}
2989
2990
	/**
2991
	 * Find the year of the earliest event.
2992
	 *
2993
	 * @return string
2994
	 */
2995
	public function firstEventYear() {
2996
		return $this->eventQuery('year', 'ASC', WT_EVENTS_BIRT . '|' . WT_EVENTS_MARR . '|' . WT_EVENTS_DIV . '|' . WT_EVENTS_DEAT);
2997
	}
2998
2999
	/**
3000
	 * Find the type of the earliest event.
3001
	 *
3002
	 * @return string
3003
	 */
3004
	public function firstEventType() {
3005
		return $this->eventQuery('type', 'ASC', WT_EVENTS_BIRT . '|' . WT_EVENTS_MARR . '|' . WT_EVENTS_DIV . '|' . WT_EVENTS_DEAT);
3006
	}
3007
3008
	/**
3009
	 * Find the name of the individual with the earliest event.
3010
	 *
3011
	 * @return string
3012
	 */
3013
	public function firstEventName() {
3014
		return $this->eventQuery('name', 'ASC', WT_EVENTS_BIRT . '|' . WT_EVENTS_MARR . '|' . WT_EVENTS_DIV . '|' . WT_EVENTS_DEAT);
3015
	}
3016
3017
	/**
3018
	 * Find the location of the earliest event.
3019
	 *
3020
	 * @return string
3021
	 */
3022
	public function firstEventPlace() {
3023
		return $this->eventQuery('place', 'ASC', WT_EVENTS_BIRT . '|' . WT_EVENTS_MARR . '|' . WT_EVENTS_DIV . '|' . WT_EVENTS_DEAT);
3024
	}
3025
3026
	/**
3027
	 * Find the latest event.
3028
	 *
3029
	 * @return string
3030
	 */
3031
	public function lastEvent() {
3032
		return $this->eventQuery('full', 'DESC', WT_EVENTS_BIRT . '|' . WT_EVENTS_MARR . '|' . WT_EVENTS_DIV . '|' . WT_EVENTS_DEAT);
3033
	}
3034
3035
	/**
3036
	 * Find the year of the latest event.
3037
	 *
3038
	 * @return string
3039
	 */
3040
	public function lastEventYear() {
3041
		return $this->eventQuery('year', 'DESC', WT_EVENTS_BIRT . '|' . WT_EVENTS_MARR . '|' . WT_EVENTS_DIV . '|' . WT_EVENTS_DEAT);
3042
	}
3043
3044
	/**
3045
	 * Find the type of the latest event.
3046
	 *
3047
	 * @return string
3048
	 */
3049
	public function lastEventType() {
3050
		return $this->eventQuery('type', 'DESC', WT_EVENTS_BIRT . '|' . WT_EVENTS_MARR . '|' . WT_EVENTS_DIV . '|' . WT_EVENTS_DEAT);
3051
	}
3052
3053
	/**
3054
	 * Find the name of the individual with the latest event.
3055
	 *
3056
	 * @return string
3057
	 */
3058
	public function lastEventName() {
3059
		return $this->eventQuery('name', 'DESC', WT_EVENTS_BIRT . '|' . WT_EVENTS_MARR . '|' . WT_EVENTS_DIV . '|' . WT_EVENTS_DEAT);
3060
	}
3061
3062
	/**
3063
	 * FInd the location of the latest event.
3064
	 *
3065
	 * @return string
3066
	 */
3067
	public function lastEventPlace() {
3068
		return $this->eventQuery('place', 'DESC', WT_EVENTS_BIRT . '|' . WT_EVENTS_MARR . '|' . WT_EVENTS_DIV . '|' . WT_EVENTS_DEAT);
3069
	}
3070
3071
	/**
3072
	 * Query the database for marriage tags.
3073
	 *
3074
	 * @param string $type
3075
	 * @param string $age_dir
3076
	 * @param string $sex
3077
	 * @param bool   $show_years
3078
	 *
3079
	 * @return string
3080
	 */
3081
	private function marriageQuery($type = 'full', $age_dir = 'ASC', $sex = 'F', $show_years = false) {
3082
		if ($sex == 'F') {
3083
			$sex_field = 'f_wife';
3084
		} else {
3085
			$sex_field = 'f_husb';
3086
		}
3087
		if ($age_dir != 'ASC') {
3088
			$age_dir = 'DESC';
3089
		}
3090
		$rows = $this->runSql(
3091
			" SELECT SQL_CACHE fam.f_id AS famid, fam.{$sex_field}, married.d_julianday2-birth.d_julianday1 AS age, indi.i_id AS i_id" .
3092
			" FROM `##families` AS fam" .
3093
			" LEFT JOIN `##dates` AS birth ON birth.d_file = {$this->tree->getTreeId()}" .
3094
			" LEFT JOIN `##dates` AS married ON married.d_file = {$this->tree->getTreeId()}" .
3095
			" LEFT JOIN `##individuals` AS indi ON indi.i_file = {$this->tree->getTreeId()}" .
3096
			" WHERE" .
3097
			" birth.d_gid = indi.i_id AND" .
3098
			" married.d_gid = fam.f_id AND" .
3099
			" indi.i_id = fam.{$sex_field} AND" .
3100
			" fam.f_file = {$this->tree->getTreeId()} AND" .
3101
			" birth.d_fact = 'BIRT' AND" .
3102
			" married.d_fact = 'MARR' AND" .
3103
			" birth.d_julianday1 <> 0 AND" .
3104
			" married.d_julianday2 > birth.d_julianday1 AND" .
3105
			" i_sex='{$sex}'" .
3106
			" ORDER BY" .
3107
			" married.d_julianday2-birth.d_julianday1 {$age_dir} LIMIT 1"
3108
		);
3109
		if (!isset($rows[0])) {
3110
			return '';
3111
		}
3112
		$row = $rows[0];
3113
		if (isset($row['famid'])) {
3114
			$family = Family::getInstance($row['famid'], $this->tree);
3115
		}
3116
		if (isset($row['i_id'])) {
3117
			$person = Individual::getInstance($row['i_id'], $this->tree);
3118
		}
3119
		switch ($type) {
3120
			default:
3121
			case 'full':
3122
				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...
3123
					$result = $family->formatList();
3124
				} else {
3125
					$result = I18N::translate('This information is private and cannot be shown.');
3126
				}
3127
				break;
3128
			case 'name':
3129
				$result = '<a href="' . e($family->url()) . '">' . $person->getFullName() . '</a>';
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...
3130
				break;
3131
			case 'age':
3132
				$age = $row['age'];
3133
				if ($show_years) {
3134
					if ((int) ($age / 365.25) > 0) {
3135
						$age = (int) ($age / 365.25) . 'y';
3136
					} elseif ((int) ($age / 30.4375) > 0) {
3137
						$age = (int) ($age / 30.4375) . 'm';
3138
					} else {
3139
						$age = $age . 'd';
3140
					}
3141
					$result = FunctionsDate::getAgeAtEvent($age);
3142
				} else {
3143
					$result = I18N::number((int) ($age / 365.25));
3144
				}
3145
				break;
3146
		}
3147
3148
		return $result;
3149
	}
3150
3151
	/**
3152
	 * General query on age at marriage.
3153
	 *
3154
	 * @param string   $type
3155
	 * @param string   $age_dir
3156
	 * @param string[] $params
3157
	 *
3158
	 * @return string
3159
	 */
3160
	private function ageOfMarriageQuery($type = 'list', $age_dir = 'ASC', $params = []) {
3161
		if (isset($params[0])) {
3162
			$total = (int) $params[0];
3163
		} else {
3164
			$total = 10;
3165
		}
3166
		if ($age_dir != 'ASC') {
3167
			$age_dir = 'DESC';
3168
		}
3169
		$hrows = $this->runSql(
3170
			" SELECT SQL_CACHE DISTINCT fam.f_id AS family, MIN(husbdeath.d_julianday2-married.d_julianday1) AS age" .
3171
			" FROM `##families` AS fam" .
3172
			" LEFT JOIN `##dates` AS married ON married.d_file = {$this->tree->getTreeId()}" .
3173
			" LEFT JOIN `##dates` AS husbdeath ON husbdeath.d_file = {$this->tree->getTreeId()}" .
3174
			" WHERE" .
3175
			" fam.f_file = {$this->tree->getTreeId()} AND" .
3176
			" husbdeath.d_gid = fam.f_husb AND" .
3177
			" husbdeath.d_fact = 'DEAT' AND" .
3178
			" married.d_gid = fam.f_id AND" .
3179
			" married.d_fact = 'MARR' AND" .
3180
			" married.d_julianday1 < husbdeath.d_julianday2 AND" .
3181
			" married.d_julianday1 <> 0" .
3182
			" GROUP BY family" .
3183
			" ORDER BY age {$age_dir}");
3184
		$wrows = $this->runSql(
3185
			" SELECT SQL_CACHE DISTINCT fam.f_id AS family, MIN(wifedeath.d_julianday2-married.d_julianday1) AS age" .
3186
			" FROM `##families` AS fam" .
3187
			" LEFT JOIN `##dates` AS married ON married.d_file = {$this->tree->getTreeId()}" .
3188
			" LEFT JOIN `##dates` AS wifedeath ON wifedeath.d_file = {$this->tree->getTreeId()}" .
3189
			" WHERE" .
3190
			" fam.f_file = {$this->tree->getTreeId()} AND" .
3191
			" wifedeath.d_gid = fam.f_wife AND" .
3192
			" wifedeath.d_fact = 'DEAT' AND" .
3193
			" married.d_gid = fam.f_id AND" .
3194
			" married.d_fact = 'MARR' AND" .
3195
			" married.d_julianday1 < wifedeath.d_julianday2 AND" .
3196
			" married.d_julianday1 <> 0" .
3197
			" GROUP BY family" .
3198
			" ORDER BY age {$age_dir}");
3199
		$drows = $this->runSql(
3200
			" SELECT SQL_CACHE DISTINCT fam.f_id AS family, MIN(divorced.d_julianday2-married.d_julianday1) AS age" .
3201
			" FROM `##families` AS fam" .
3202
			" LEFT JOIN `##dates` AS married ON married.d_file = {$this->tree->getTreeId()}" .
3203
			" LEFT JOIN `##dates` AS divorced ON divorced.d_file = {$this->tree->getTreeId()}" .
3204
			" WHERE" .
3205
			" fam.f_file = {$this->tree->getTreeId()} AND" .
3206
			" married.d_gid = fam.f_id AND" .
3207
			" married.d_fact = 'MARR' AND" .
3208
			" divorced.d_gid = fam.f_id AND" .
3209
			" divorced.d_fact IN ('DIV', 'ANUL', '_SEPR', '_DETS') AND" .
3210
			" married.d_julianday1 < divorced.d_julianday2 AND" .
3211
			" married.d_julianday1 <> 0" .
3212
			" GROUP BY family" .
3213
			" ORDER BY age {$age_dir}");
3214
		if (!isset($hrows) && !isset($wrows) && !isset($drows)) {
3215
			return '';
3216
		}
3217
		$rows = [];
3218
		foreach ($drows as $family) {
3219
			$rows[$family['family']] = $family['age'];
3220
		}
3221
		foreach ($hrows as $family) {
3222
			if (!isset($rows[$family['family']])) {
3223
				$rows[$family['family']] = $family['age'];
3224
			}
3225
		}
3226
		foreach ($wrows as $family) {
3227
			if (!isset($rows[$family['family']])) {
3228
				$rows[$family['family']] = $family['age'];
3229
			} elseif ($rows[$family['family']] > $family['age']) {
3230
				$rows[$family['family']] = $family['age'];
3231
			}
3232
		}
3233
		if ($age_dir === 'DESC') {
3234
			arsort($rows);
3235
		} else {
3236
			asort($rows);
3237
		}
3238
		$top10 = [];
3239
		$i     = 0;
3240
		foreach ($rows as $fam => $age) {
3241
			$family = Family::getInstance($fam, $this->tree);
3242
			if ($type === 'name') {
3243
				return $family->formatList();
3244
			}
3245
			if ((int) ($age / 365.25) > 0) {
3246
				$age = (int) ($age / 365.25) . 'y';
3247
			} elseif ((int) ($age / 30.4375) > 0) {
3248
				$age = (int) ($age / 30.4375) . 'm';
3249
			} else {
3250
				$age = $age . 'd';
3251
			}
3252
			$age = FunctionsDate::getAgeAtEvent($age);
3253
			if ($type === 'age') {
3254
				return $age;
3255
			}
3256
			$husb = $family->getHusband();
0 ignored issues
show
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

3256
			/** @scrutinizer ignore-call */ 
3257
   $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

3256
			/** @scrutinizer ignore-call */ 
3257
   $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

3256
			/** @scrutinizer ignore-call */ 
3257
   $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

3256
			/** @scrutinizer ignore-call */ 
3257
   $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\Individual. ( Ignorable by Annotation )

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

3256
			/** @scrutinizer ignore-call */ 
3257
   $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

3256
			/** @scrutinizer ignore-call */ 
3257
   $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...
3257
			$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

3257
			/** @scrutinizer ignore-call */ 
3258
   $wife = $family->getWife();
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

3257
			/** @scrutinizer ignore-call */ 
3258
   $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

3257
			/** @scrutinizer ignore-call */ 
3258
   $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

3257
			/** @scrutinizer ignore-call */ 
3258
   $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\Individual. ( Ignorable by Annotation )

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

3257
			/** @scrutinizer ignore-call */ 
3258
   $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

3257
			/** @scrutinizer ignore-call */ 
3258
   $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...
3258
			if ($husb && $wife && ($husb->getAllDeathDates() && $wife->getAllDeathDates() || !$husb->isDead() || !$wife->isDead())) {
3259
				if ($family->canShow()) {
3260
					if ($type === 'list') {
3261
						$top10[] = '<li><a href="' . e($family->url()) . '">' . $family->getFullName() . '</a> (' . $age . ')' . '</li>';
3262
					} else {
3263
						$top10[] = '<a href="' . e($family->url()) . '">' . $family->getFullName() . '</a> (' . $age . ')';
3264
					}
3265
				}
3266
				if (++$i === $total) {
3267
					break;
3268
				}
3269
			}
3270
		}
3271
		if ($type === 'list') {
3272
			$top10 = implode('', $top10);
3273
		} else {
3274
			$top10 = implode('; ', $top10);
3275
		}
3276
		if (I18N::direction() === 'rtl') {
3277
			$top10 = str_replace(['[', ']', '(', ')', '+'], ['&rlm;[', '&rlm;]', '&rlm;(', '&rlm;)', '&rlm;+'], $top10);
3278
		}
3279
		if ($type === 'list') {
3280
			return '<ul>' . $top10 . '</ul>';
3281
		}
3282
3283
		return $top10;
3284
	}
3285
3286
	/**
3287
	 * Find the ages between spouses.
3288
	 *
3289
	 * @param string   $type
3290
	 * @param string   $age_dir
3291
	 * @param string[] $params
3292
	 *
3293
	 * @return string
3294
	 */
3295
	private function ageBetweenSpousesQuery($type = 'list', $age_dir = 'DESC', $params = []) {
3296
		if (isset($params[0])) {
3297
			$total = (int) $params[0];
3298
		} else {
3299
			$total = 10;
3300
		}
3301
		if ($age_dir === 'DESC') {
3302
			$sql =
3303
				"SELECT SQL_CACHE f_id AS xref, MIN(wife.d_julianday2-husb.d_julianday1) AS age" .
3304
				" FROM `##families`" .
3305
				" JOIN `##dates` AS wife ON wife.d_gid = f_wife AND wife.d_file = f_file" .
3306
				" JOIN `##dates` AS husb ON husb.d_gid = f_husb AND husb.d_file = f_file" .
3307
				" WHERE f_file = :tree_id" .
3308
				" AND husb.d_fact = 'BIRT'" .
3309
				" AND wife.d_fact = 'BIRT'" .
3310
				" AND wife.d_julianday2 >= husb.d_julianday1 AND husb.d_julianday1 <> 0" .
3311
				" GROUP BY xref" .
3312
				" ORDER BY age DESC" .
3313
				" LIMIT :limit";
3314
		} else {
3315
			$sql =
3316
				"SELECT SQL_CACHE f_id AS xref, MIN(husb.d_julianday2-wife.d_julianday1) AS age" .
3317
				" FROM `##families`" .
3318
				" JOIN `##dates` AS wife ON wife.d_gid = f_wife AND wife.d_file = f_file" .
3319
				" JOIN `##dates` AS husb ON husb.d_gid = f_husb AND husb.d_file = f_file" .
3320
				" WHERE f_file = :tree_id" .
3321
				" AND husb.d_fact = 'BIRT'" .
3322
				" AND wife.d_fact = 'BIRT'" .
3323
				" AND husb.d_julianday2 >= wife.d_julianday1 AND wife.d_julianday1 <> 0" .
3324
				" GROUP BY xref" .
3325
				" ORDER BY age DESC" .
3326
				" LIMIT :limit";
3327
		}
3328
		$rows = Database::prepare(
3329
			$sql
3330
		)->execute([
3331
			'tree_id' => $this->tree->getTreeId(),
3332
			'limit'   => $total,
3333
		])->fetchAll();
3334
3335
		$top10 = [];
3336
		foreach ($rows as $fam) {
3337
			$family = Family::getInstance($fam->xref, $this->tree);
3338
			if ($fam->age < 0) {
3339
				break;
3340
			}
3341
			$age = $fam->age;
3342
			if ((int) ($age / 365.25) > 0) {
3343
				$age = (int) ($age / 365.25) . 'y';
3344
			} elseif ((int) ($age / 30.4375) > 0) {
3345
				$age = (int) ($age / 30.4375) . 'm';
3346
			} else {
3347
				$age = $age . 'd';
3348
			}
3349
			$age = FunctionsDate::getAgeAtEvent($age);
3350
			if ($family->canShow()) {
3351
				if ($type === 'list') {
3352
					$top10[] = '<li><a href="' . e($family->url()) . '">' . $family->getFullName() . '</a> (' . $age . ')' . '</li>';
3353
				} else {
3354
					$top10[] = '<a href="' . e($family->url()) . '">' . $family->getFullName() . '</a> (' . $age . ')';
3355
				}
3356
			}
3357
		}
3358
		if ($type === 'list') {
3359
			$top10 = implode('', $top10);
3360
			if ($top10) {
3361
				$top10 = '<ul>' . $top10 . '</ul>';
3362
			}
3363
		} else {
3364
			$top10 = implode(' ', $top10);
3365
		}
3366
3367
		return $top10;
3368
	}
3369
3370
	/**
3371
	 * General query on parents.
3372
	 *
3373
	 * @param string $type
3374
	 * @param string $age_dir
3375
	 * @param string $sex
3376
	 * @param bool   $show_years
3377
	 *
3378
	 * @return string
3379
	 */
3380
	private function parentsQuery($type = 'full', $age_dir = 'ASC', $sex = 'F', $show_years = false) {
3381
		if ($sex == 'F') {
3382
			$sex_field = 'WIFE';
3383
		} else {
3384
			$sex_field = 'HUSB';
3385
		}
3386
		if ($age_dir != 'ASC') {
3387
			$age_dir = 'DESC';
3388
		}
3389
		$rows = $this->runSql(
3390
			" SELECT SQL_CACHE" .
3391
			" parentfamily.l_to AS id," .
3392
			" childbirth.d_julianday2-birth.d_julianday1 AS age" .
3393
			" FROM `##link` AS parentfamily" .
3394
			" JOIN `##link` AS childfamily ON childfamily.l_file = {$this->tree->getTreeId()}" .
3395
			" JOIN `##dates` AS birth ON birth.d_file = {$this->tree->getTreeId()}" .
3396
			" JOIN `##dates` AS childbirth ON childbirth.d_file = {$this->tree->getTreeId()}" .
3397
			" WHERE" .
3398
			" birth.d_gid = parentfamily.l_to AND" .
3399
			" childfamily.l_to = childbirth.d_gid AND" .
3400
			" childfamily.l_type = 'CHIL' AND" .
3401
			" parentfamily.l_type = '{$sex_field}' AND" .
3402
			" childfamily.l_from = parentfamily.l_from AND" .
3403
			" parentfamily.l_file = {$this->tree->getTreeId()} AND" .
3404
			" birth.d_fact = 'BIRT' AND" .
3405
			" childbirth.d_fact = 'BIRT' AND" .
3406
			" birth.d_julianday1 <> 0 AND" .
3407
			" childbirth.d_julianday2 > birth.d_julianday1" .
3408
			" ORDER BY age {$age_dir} LIMIT 1"
3409
		);
3410
		if (!isset($rows[0])) {
3411
			return '';
3412
		}
3413
		$row = $rows[0];
3414
		if (isset($row['id'])) {
3415
			$person = Individual::getInstance($row['id'], $this->tree);
3416
		}
3417
		switch ($type) {
3418
			default:
3419
			case 'full':
3420
				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...
3421
					$result = $person->formatList();
3422
				} else {
3423
					$result = I18N::translate('This information is private and cannot be shown.');
3424
				}
3425
				break;
3426
			case 'name':
3427
				$result = '<a href="' . e($person->url()) . '">' . $person->getFullName() . '</a>';
3428
				break;
3429
			case 'age':
3430
				$age = $row['age'];
3431
				if ($show_years) {
3432
					if ((int) ($age / 365.25) > 0) {
3433
						$age = (int) ($age / 365.25) . 'y';
3434
					} elseif ((int) ($age / 30.4375) > 0) {
3435
						$age = (int) ($age / 30.4375) . 'm';
3436
					} else {
3437
						$age = $age . 'd';
3438
					}
3439
					$result = FunctionsDate::getAgeAtEvent($age);
3440
				} else {
3441
					$result = (int) ($age / 365.25);
3442
				}
3443
				break;
3444
		}
3445
3446
		return $result;
3447
	}
3448
3449
	/**
3450
	 * General query on marriages.
3451
	 *
3452
	 * @param bool     $simple
3453
	 * @param bool     $first
3454
	 * @param int      $year1
3455
	 * @param int      $year2
3456
	 * @param string[] $params
3457
	 *
3458
	 * @return string|array
3459
	 */
3460
	public function statsMarrQuery($simple = true, $first = false, $year1 = -1, $year2 = -1, $params = []) {
3461
		$WT_STATS_CHART_COLOR1 = Theme::theme()->parameter('distribution-chart-no-values');
3462
		$WT_STATS_CHART_COLOR2 = Theme::theme()->parameter('distribution-chart-high-values');
3463
		$WT_STATS_S_CHART_X    = Theme::theme()->parameter('stats-small-chart-x');
3464
		$WT_STATS_S_CHART_Y    = Theme::theme()->parameter('stats-small-chart-y');
3465
3466
		if ($simple) {
3467
			$sql =
3468
				"SELECT SQL_CACHE FLOOR(d_year/100+1) AS century, COUNT(*) AS total" .
3469
				" FROM `##dates`" .
3470
				" WHERE d_file={$this->tree->getTreeId()} AND d_year<>0 AND d_fact='MARR' AND d_type IN ('@#DGREGORIAN@', '@#DJULIAN@')";
3471
			if ($year1 >= 0 && $year2 >= 0) {
3472
				$sql .= " AND d_year BETWEEN '{$year1}' AND '{$year2}'";
3473
			}
3474
			$sql .= " GROUP BY century ORDER BY century";
3475
		} elseif ($first) {
3476
			$years = '';
3477
			if ($year1 >= 0 && $year2 >= 0) {
3478
				$years = " married.d_year BETWEEN '{$year1}' AND '{$year2}' AND";
3479
			}
3480
			$sql =
3481
				" SELECT SQL_CACHE 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" .
3482
				" FROM `##families` AS fam" .
3483
				" LEFT JOIN `##dates` AS married ON married.d_file = {$this->tree->getTreeId()}" .
3484
				" LEFT JOIN `##individuals` AS indi ON indi.i_file = {$this->tree->getTreeId()}" .
3485
				" WHERE" .
3486
				" married.d_gid = fam.f_id AND" .
3487
				" fam.f_file = {$this->tree->getTreeId()} AND" .
3488
				" married.d_fact = 'MARR' AND" .
3489
				" married.d_julianday2 <> 0 AND" .
3490
				$years .
3491
				" (indi.i_id = fam.f_husb OR indi.i_id = fam.f_wife)" .
3492
				" ORDER BY fams, indi, age ASC";
3493
		} else {
3494
			$sql =
3495
				"SELECT SQL_CACHE d_month, COUNT(*) AS total" .
3496
				" FROM `##dates`" .
3497
				" WHERE d_file={$this->tree->getTreeId()} AND d_fact='MARR'";
3498
			if ($year1 >= 0 && $year2 >= 0) {
3499
				$sql .= " AND d_year BETWEEN '{$year1}' AND '{$year2}'";
3500
			}
3501
			$sql .= " GROUP BY d_month";
3502
		}
3503
		$rows = $this->runSql($sql);
3504
		if (!isset($rows)) {
3505
			return '';
3506
		}
3507
		if ($simple) {
3508
			if (isset($params[0]) && $params[0] != '') {
3509
				$size = strtolower($params[0]);
3510
			} else {
3511
				$size = $WT_STATS_S_CHART_X . 'x' . $WT_STATS_S_CHART_Y;
3512
			}
3513
			if (isset($params[1]) && $params[1] != '') {
3514
				$color_from = strtolower($params[1]);
3515
			} else {
3516
				$color_from = $WT_STATS_CHART_COLOR1;
3517
			}
3518
			if (isset($params[2]) && $params[2] != '') {
3519
				$color_to = strtolower($params[2]);
3520
			} else {
3521
				$color_to = $WT_STATS_CHART_COLOR2;
3522
			}
3523
			$sizes = explode('x', $size);
3524
			$tot   = 0;
3525
			foreach ($rows as $values) {
3526
				$tot += (int) $values['total'];
3527
			}
3528
			// Beware divide by zero
3529
			if ($tot === 0) {
0 ignored issues
show
introduced by
The condition $tot === 0 can never be false.
Loading history...
3530
				return '';
3531
			}
3532
			$centuries = '';
3533
			$counts    = [];
3534
			foreach ($rows as $values) {
3535
				$counts[] = round(100 * $values['total'] / $tot, 0);
3536
				$centuries .= $this->centuryName($values['century']) . ' - ' . I18N::number($values['total']) . '|';
3537
			}
3538
			$chd = $this->arrayToExtendedEncoding($counts);
3539
			$chl = substr($centuries, 0, -1);
3540
3541
			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') . '" />';
3542
		}
3543
3544
		return $rows;
3545
	}
3546
3547
	/**
3548
	 * General query on divorces.
3549
	 *
3550
	 * @param bool     $simple
3551
	 * @param bool     $first
3552
	 * @param int      $year1
3553
	 * @param int      $year2
3554
	 * @param string[] $params
3555
	 *
3556
	 * @return string|array
3557
	 */
3558
	private function statsDivQuery($simple = true, $first = false, $year1 = -1, $year2 = -1, $params = []) {
3559
		$WT_STATS_CHART_COLOR1 = Theme::theme()->parameter('distribution-chart-no-values');
3560
		$WT_STATS_CHART_COLOR2 = Theme::theme()->parameter('distribution-chart-high-values');
3561
		$WT_STATS_S_CHART_X    = Theme::theme()->parameter('stats-small-chart-x');
3562
		$WT_STATS_S_CHART_Y    = Theme::theme()->parameter('stats-small-chart-y');
3563
3564
		if ($simple) {
3565
			$sql =
3566
				"SELECT SQL_CACHE FLOOR(d_year/100+1) AS century, COUNT(*) AS total" .
3567
				" FROM `##dates`" .
3568
				" WHERE d_file={$this->tree->getTreeId()} AND d_year<>0 AND d_fact = 'DIV' AND d_type IN ('@#DGREGORIAN@', '@#DJULIAN@')";
3569
			if ($year1 >= 0 && $year2 >= 0) {
3570
				$sql .= " AND d_year BETWEEN '{$year1}' AND '{$year2}'";
3571
			}
3572
			$sql .= " GROUP BY century ORDER BY century";
3573
		} elseif ($first) {
3574
			$years = '';
3575
			if ($year1 >= 0 && $year2 >= 0) {
3576
				$years = " divorced.d_year BETWEEN '{$year1}' AND '{$year2}' AND";
3577
			}
3578
			$sql =
3579
				" SELECT SQL_CACHE 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" .
3580
				" FROM `##families` AS fam" .
3581
				" LEFT JOIN `##dates` AS divorced ON divorced.d_file = {$this->tree->getTreeId()}" .
3582
				" LEFT JOIN `##individuals` AS indi ON indi.i_file = {$this->tree->getTreeId()}" .
3583
				" WHERE" .
3584
				" divorced.d_gid = fam.f_id AND" .
3585
				" fam.f_file = {$this->tree->getTreeId()} AND" .
3586
				" divorced.d_fact = 'DIV' AND" .
3587
				" divorced.d_julianday2 <> 0 AND" .
3588
				$years .
3589
				" (indi.i_id = fam.f_husb OR indi.i_id = fam.f_wife)" .
3590
				" ORDER BY fams, indi, age ASC";
3591
		} else {
3592
			$sql =
3593
				"SELECT SQL_CACHE d_month, COUNT(*) AS total FROM `##dates` " .
3594
				"WHERE d_file={$this->tree->getTreeId()} AND d_fact = 'DIV'";
3595
			if ($year1 >= 0 && $year2 >= 0) {
3596
				$sql .= " AND d_year BETWEEN '{$year1}' AND '{$year2}'";
3597
			}
3598
			$sql .= " GROUP BY d_month";
3599
		}
3600
		$rows = $this->runSql($sql);
3601
		if (!isset($rows)) {
3602
			return '';
3603
		}
3604
		if ($simple) {
3605
			if (isset($params[0]) && $params[0] != '') {
3606
				$size = strtolower($params[0]);
3607
			} else {
3608
				$size = $WT_STATS_S_CHART_X . 'x' . $WT_STATS_S_CHART_Y;
3609
			}
3610
			if (isset($params[1]) && $params[1] != '') {
3611
				$color_from = strtolower($params[1]);
3612
			} else {
3613
				$color_from = $WT_STATS_CHART_COLOR1;
3614
			}
3615
			if (isset($params[2]) && $params[2] != '') {
3616
				$color_to = strtolower($params[2]);
3617
			} else {
3618
				$color_to = $WT_STATS_CHART_COLOR2;
3619
			}
3620
			$sizes = explode('x', $size);
3621
			$tot   = 0;
3622
			foreach ($rows as $values) {
3623
				$tot += (int) $values['total'];
3624
			}
3625
			// Beware divide by zero
3626
			if ($tot === 0) {
0 ignored issues
show
introduced by
The condition $tot === 0 can never be false.
Loading history...
3627
				return '';
3628
			}
3629
			$centuries = '';
3630
			$counts    = [];
3631
			foreach ($rows as $values) {
3632
				$counts[] = round(100 * $values['total'] / $tot, 0);
3633
				$centuries .= $this->centuryName($values['century']) . ' - ' . I18N::number($values['total']) . '|';
3634
			}
3635
			$chd = $this->arrayToExtendedEncoding($counts);
3636
			$chl = substr($centuries, 0, -1);
3637
3638
			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') . '" />';
3639
		}
3640
3641
		return $rows;
3642
	}
3643
3644
	/**
3645
	 * Find the earliest marriage.
3646
	 *
3647
	 * @return string
3648
	 */
3649
	public function firstMarriage() {
3650
		return $this->mortalityQuery('full', 'ASC', 'MARR');
3651
	}
3652
3653
	/**
3654
	 * Find the year of the earliest marriage.
3655
	 *
3656
	 * @return string
3657
	 */
3658
	public function firstMarriageYear() {
3659
		return $this->mortalityQuery('year', 'ASC', 'MARR');
3660
	}
3661
3662
	/**
3663
	 * Find the names of spouses of the earliest marriage.
3664
	 *
3665
	 * @return string
3666
	 */
3667
	public function firstMarriageName() {
3668
		return $this->mortalityQuery('name', 'ASC', 'MARR');
3669
	}
3670
3671
	/**
3672
	 * Find the place of the earliest marriage.
3673
	 *
3674
	 * @return string
3675
	 */
3676
	public function firstMarriagePlace() {
3677
		return $this->mortalityQuery('place', 'ASC', 'MARR');
3678
	}
3679
3680
	/**
3681
	 * Find the latest marriage.
3682
	 *
3683
	 * @return string
3684
	 */
3685
	public function lastMarriage() {
3686
		return $this->mortalityQuery('full', 'DESC', 'MARR');
3687
	}
3688
3689
	/**
3690
	 * Find the year of the latest marriage.
3691
	 *
3692
	 * @return string
3693
	 */
3694
	public function lastMarriageYear() {
3695
		return $this->mortalityQuery('year', 'DESC', 'MARR');
3696
	}
3697
3698
	/**
3699
	 * Find the names of spouses of the latest marriage.
3700
	 *
3701
	 * @return string
3702
	 */
3703
	public function lastMarriageName() {
3704
		return $this->mortalityQuery('name', 'DESC', 'MARR');
3705
	}
3706
3707
	/**
3708
	 * Find the location of the latest marriage.
3709
	 *
3710
	 * @return string
3711
	 */
3712
	public function lastMarriagePlace() {
3713
		return $this->mortalityQuery('place', 'DESC', 'MARR');
3714
	}
3715
3716
	/**
3717
	 * General query on marriages.
3718
	 *
3719
	 * @param string[] $params
3720
	 *
3721
	 * @return string
3722
	 */
3723
	public function statsMarr($params = []) {
3724
		return $this->statsMarrQuery(true, false, -1, -1, $params);
3725
	}
3726
3727
	/**
3728
	 * Find the earliest divorce.
3729
	 *
3730
	 * @return string
3731
	 */
3732
	public function firstDivorce() {
3733
		return $this->mortalityQuery('full', 'ASC', 'DIV');
3734
	}
3735
3736
	/**
3737
	 * Find the year of the earliest divorce.
3738
	 *
3739
	 * @return string
3740
	 */
3741
	public function firstDivorceYear() {
3742
		return $this->mortalityQuery('year', 'ASC', 'DIV');
3743
	}
3744
3745
	/**
3746
	 * Find the names of individuals in the earliest divorce.
3747
	 *
3748
	 * @return string
3749
	 */
3750
	public function firstDivorceName() {
3751
		return $this->mortalityQuery('name', 'ASC', 'DIV');
3752
	}
3753
3754
	/**
3755
	 * Find the location of the earliest divorce.
3756
	 *
3757
	 * @return string
3758
	 */
3759
	public function firstDivorcePlace() {
3760
		return $this->mortalityQuery('place', 'ASC', 'DIV');
3761
	}
3762
3763
	/**
3764
	 * Find the latest divorce.
3765
	 *
3766
	 * @return string
3767
	 */
3768
	public function lastDivorce() {
3769
		return $this->mortalityQuery('full', 'DESC', 'DIV');
3770
	}
3771
3772
	/**
3773
	 * Find the year of the latest divorce.
3774
	 *
3775
	 * @return string
3776
	 */
3777
	public function lastDivorceYear() {
3778
		return $this->mortalityQuery('year', 'DESC', 'DIV');
3779
	}
3780
3781
	/**
3782
	 * Find the names of the individuals in the latest divorce.
3783
	 *
3784
	 * @return string
3785
	 */
3786
	public function lastDivorceName() {
3787
		return $this->mortalityQuery('name', 'DESC', 'DIV');
3788
	}
3789
3790
	/**
3791
	 * Find the location of the latest divorce.
3792
	 *
3793
	 * @return string
3794
	 */
3795
	public function lastDivorcePlace() {
3796
		return $this->mortalityQuery('place', 'DESC', 'DIV');
3797
	}
3798
3799
	/**
3800
	 * General divorce query.
3801
	 *
3802
	 * @param string[] $params
3803
	 *
3804
	 * @return string
3805
	 */
3806
	public function statsDiv($params = []) {
3807
		return $this->statsDivQuery(true, false, -1, -1, $params);
3808
	}
3809
3810
	/**
3811
	 * General query on ages at marriage.
3812
	 *
3813
	 * @param bool     $simple
3814
	 * @param string   $sex
3815
	 * @param int      $year1
3816
	 * @param int      $year2
3817
	 * @param string[] $params
3818
	 *
3819
	 * @return array|string
3820
	 */
3821
	public function statsMarrAgeQuery($simple = true, $sex = 'M', $year1 = -1, $year2 = -1, $params = []) {
3822
		if ($simple) {
3823
			if (isset($params[0]) && $params[0] != '') {
3824
				$size = strtolower($params[0]);
3825
			} else {
3826
				$size = '200x250';
3827
			}
3828
			$sizes = explode('x', $size);
3829
			$rows  = $this->runSql(
3830
				"SELECT SQL_CACHE " .
3831
				" ROUND(AVG(married.d_julianday2-birth.d_julianday1-182.5)/365.25,1) AS age, " .
3832
				" FLOOR(married.d_year/100+1) AS century, " .
3833
				" 'M' AS sex " .
3834
				"FROM `##dates` AS married " .
3835
				"JOIN `##families` AS fam ON (married.d_gid=fam.f_id AND married.d_file=fam.f_file) " .
3836
				"JOIN `##dates` AS birth ON (birth.d_gid=fam.f_husb AND birth.d_file=fam.f_file) " .
3837
				"WHERE " .
3838
				" '{$sex}' IN ('M', 'BOTH') AND " .
3839
				" married.d_file={$this->tree->getTreeId()} AND married.d_type IN ('@#DGREGORIAN@', '@#DJULIAN@') AND married.d_fact='MARR' AND " .
3840
				" birth.d_type IN ('@#DGREGORIAN@', '@#DJULIAN@') AND birth.d_fact='BIRT' AND " .
3841
				" married.d_julianday1>birth.d_julianday1 AND birth.d_julianday1<>0 " .
3842
				"GROUP BY century, sex " .
3843
				"UNION ALL " .
3844
				"SELECT " .
3845
				" ROUND(AVG(married.d_julianday2-birth.d_julianday1-182.5)/365.25,1) AS age, " .
3846
				" FLOOR(married.d_year/100+1) AS century, " .
3847
				" 'F' AS sex " .
3848
				"FROM `##dates` AS married " .
3849
				"JOIN `##families` AS fam ON (married.d_gid=fam.f_id AND married.d_file=fam.f_file) " .
3850
				"JOIN `##dates` AS birth ON (birth.d_gid=fam.f_wife AND birth.d_file=fam.f_file) " .
3851
				"WHERE " .
3852
				" '{$sex}' IN ('F', 'BOTH') AND " .
3853
				" married.d_file={$this->tree->getTreeId()} AND married.d_type IN ('@#DGREGORIAN@', '@#DJULIAN@') AND married.d_fact='MARR' AND " .
3854
				" birth.d_type IN ('@#DGREGORIAN@', '@#DJULIAN@') AND birth.d_fact='BIRT' AND " .
3855
				" married.d_julianday1>birth.d_julianday1 AND birth.d_julianday1<>0 " .
3856
				" GROUP BY century, sex ORDER BY century"
3857
			);
3858
			if (empty($rows)) {
3859
				return '';
3860
			}
3861
			$max = 0;
3862
			foreach ($rows as $values) {
3863
				if ($max < $values['age']) {
3864
					$max = $values['age'];
3865
				}
3866
			}
3867
			$chxl    = '0:|';
3868
			$chmm    = '';
3869
			$chmf    = '';
3870
			$i       = 0;
3871
			$countsm = '';
3872
			$countsf = '';
3873
			$countsa = '';
3874
			$out     = [];
3875
			foreach ($rows as $values) {
3876
				$out[$values['century']][$values['sex']] = $values['age'];
3877
			}
3878
			foreach ($out as $century => $values) {
3879
				if ($sizes[0] < 1000) {
3880
					$sizes[0] += 50;
3881
				}
3882
				$chxl .= $this->centuryName($century) . '|';
3883
				$average = 0;
3884
				if (isset($values['F'])) {
3885
					if ($max <= 50) {
3886
						$value = $values['F'] * 2;
3887
					} else {
3888
						$value = $values['F'];
3889
					}
3890
					$countsf .= $value . ',';
3891
					$average = $value;
3892
					$chmf .= 't' . $values['F'] . ',000000,1,' . $i . ',11,1|';
3893
				} else {
3894
					$countsf .= '0,';
3895
					$chmf .= 't0,000000,1,' . $i . ',11,1|';
3896
				}
3897
				if (isset($values['M'])) {
3898
					if ($max <= 50) {
3899
						$value = $values['M'] * 2;
3900
					} else {
3901
						$value = $values['M'];
3902
					}
3903
					$countsm .= $value . ',';
3904
					if ($average == 0) {
3905
						$countsa .= $value . ',';
3906
					} else {
3907
						$countsa .= (($value + $average) / 2) . ',';
3908
					}
3909
					$chmm .= 't' . $values['M'] . ',000000,0,' . $i . ',11,1|';
3910
				} else {
3911
					$countsm .= '0,';
3912
					if ($average == 0) {
3913
						$countsa .= '0,';
3914
					} else {
3915
						$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...
3916
					}
3917
					$chmm .= 't0,000000,0,' . $i . ',11,1|';
3918
				}
3919
				$i++;
3920
			}
3921
			$countsm = substr($countsm, 0, -1);
3922
			$countsf = substr($countsf, 0, -1);
3923
			$countsa = substr($countsa, 0, -1);
3924
			$chmf    = substr($chmf, 0, -1);
3925
			$chd     = 't2:' . $countsm . '|' . $countsf . '|' . $countsa;
3926
			if ($max <= 50) {
3927
				$chxl .= '1:||' . I18N::translate('century') . '|2:|0|10|20|30|40|50|3:||' . I18N::translate('Age') . '|';
3928
			} else {
3929
				$chxl .= '1:||' . I18N::translate('century') . '|2:|0|10|20|30|40|50|60|70|80|90|100|3:||' . I18N::translate('Age') . '|';
3930
			}
3931
			if (count($rows) > 4 || mb_strlen(I18N::translate('Average age in century of marriage')) < 30) {
3932
				$chtt = I18N::translate('Average age in century of marriage');
3933
			} else {
3934
				$offset  = 0;
3935
				$counter = [];
3936
				while ($offset = strpos(I18N::translate('Average age in century of marriage'), ' ', $offset + 1)) {
3937
					$counter[] = $offset;
3938
				}
3939
				$half = (int) (count($counter) / 2);
3940
				$chtt = substr_replace(I18N::translate('Average age in century of marriage'), '|', $counter[$half], 1);
3941
			}
3942
3943
			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') . '" />';
3944
		} else {
3945
			if ($year1 >= 0 && $year2 >= 0) {
3946
				$years = " married.d_year BETWEEN {$year1} AND {$year2} AND ";
3947
			} else {
3948
				$years = '';
3949
			}
3950
			$rows = $this->runSql(
3951
				"SELECT SQL_CACHE " .
3952
				" fam.f_id, " .
3953
				" birth.d_gid, " .
3954
				" married.d_julianday2-birth.d_julianday1 AS age " .
3955
				"FROM `##dates` AS married " .
3956
				"JOIN `##families` AS fam ON (married.d_gid=fam.f_id AND married.d_file=fam.f_file) " .
3957
				"JOIN `##dates` AS birth ON (birth.d_gid=fam.f_husb AND birth.d_file=fam.f_file) " .
3958
				"WHERE " .
3959
				" '{$sex}' IN ('M', 'BOTH') AND {$years} " .
3960
				" married.d_file={$this->tree->getTreeId()} AND married.d_type IN ('@#DGREGORIAN@', '@#DJULIAN@') AND married.d_fact='MARR' AND " .
3961
				" birth.d_type IN ('@#DGREGORIAN@', '@#DJULIAN@') AND birth.d_fact='BIRT' AND " .
3962
				" married.d_julianday1>birth.d_julianday1 AND birth.d_julianday1<>0 " .
3963
				"UNION ALL " .
3964
				"SELECT " .
3965
				" fam.f_id, " .
3966
				" birth.d_gid, " .
3967
				" married.d_julianday2-birth.d_julianday1 AS age " .
3968
				"FROM `##dates` AS married " .
3969
				"JOIN `##families` AS fam ON (married.d_gid=fam.f_id AND married.d_file=fam.f_file) " .
3970
				"JOIN `##dates` AS birth ON (birth.d_gid=fam.f_wife AND birth.d_file=fam.f_file) " .
3971
				"WHERE " .
3972
				" '{$sex}' IN ('F', 'BOTH') AND {$years} " .
3973
				" married.d_file={$this->tree->getTreeId()} AND married.d_type IN ('@#DGREGORIAN@', '@#DJULIAN@') AND married.d_fact='MARR' AND " .
3974
				" birth.d_type IN ('@#DGREGORIAN@', '@#DJULIAN@') AND birth.d_fact='BIRT' AND " .
3975
				" married.d_julianday1>birth.d_julianday1 AND birth.d_julianday1<>0 "
3976
			);
3977
3978
			return $rows;
3979
		}
3980
	}
3981
3982
	/**
3983
	 * Find the youngest wife.
3984
	 *
3985
	 * @return string
3986
	 */
3987
	public function youngestMarriageFemale() {
3988
		return $this->marriageQuery('full', 'ASC', 'F', false);
3989
	}
3990
3991
	/**
3992
	 * Find the name of the youngest wife.
3993
	 *
3994
	 * @return string
3995
	 */
3996
	public function youngestMarriageFemaleName() {
3997
		return $this->marriageQuery('name', 'ASC', 'F', false);
3998
	}
3999
4000
	/**
4001
	 * Find the age of the youngest wife.
4002
	 *
4003
	 * @param bool $show_years
4004
	 *
4005
	 * @return string
4006
	 */
4007
	public function youngestMarriageFemaleAge($show_years = false) {
4008
		return $this->marriageQuery('age', 'ASC', 'F', $show_years);
4009
	}
4010
4011
	/**
4012
	 * Find the oldest wife.
4013
	 *
4014
	 * @return string
4015
	 */
4016
	public function oldestMarriageFemale() {
4017
		return $this->marriageQuery('full', 'DESC', 'F', false);
4018
	}
4019
4020
	/**
4021
	 * Find the name of the oldest wife.
4022
	 *
4023
	 * @return string
4024
	 */
4025
	public function oldestMarriageFemaleName() {
4026
		return $this->marriageQuery('name', 'DESC', 'F', false);
4027
	}
4028
4029
	/**
4030
	 * Find the age of the oldest wife.
4031
	 *
4032
	 * @param bool $show_years
4033
	 *
4034
	 * @return string
4035
	 */
4036
	public function oldestMarriageFemaleAge($show_years = false) {
4037
		return $this->marriageQuery('age', 'DESC', 'F', $show_years);
4038
	}
4039
4040
	/**
4041
	 * Find the youngest husband.
4042
	 *
4043
	 * @return string
4044
	 */
4045
	public function youngestMarriageMale() {
4046
		return $this->marriageQuery('full', 'ASC', 'M', false);
4047
	}
4048
4049
	/**
4050
	 * Find the name of the youngest husband.
4051
	 *
4052
	 * @return string
4053
	 */
4054
	public function youngestMarriageMaleName() {
4055
		return $this->marriageQuery('name', 'ASC', 'M', false);
4056
	}
4057
4058
	/**
4059
	 * Find the age of the youngest husband.
4060
	 *
4061
	 * @param bool $show_years
4062
	 *
4063
	 * @return string
4064
	 */
4065
	public function youngestMarriageMaleAge($show_years = false) {
4066
		return $this->marriageQuery('age', 'ASC', 'M', $show_years);
4067
	}
4068
4069
	/**
4070
	 * Find the oldest husband.
4071
	 *
4072
	 * @return string
4073
	 */
4074
	public function oldestMarriageMale() {
4075
		return $this->marriageQuery('full', 'DESC', 'M', false);
4076
	}
4077
4078
	/**
4079
	 * Find the name of the oldest husband.
4080
	 *
4081
	 * @return string
4082
	 */
4083
	public function oldestMarriageMaleName() {
4084
		return $this->marriageQuery('name', 'DESC', 'M', false);
4085
	}
4086
4087
	/**
4088
	 * Find the age of the oldest husband.
4089
	 *
4090
	 * @param bool $show_years
4091
	 *
4092
	 * @return string
4093
	 */
4094
	public function oldestMarriageMaleAge($show_years = false) {
4095
		return $this->marriageQuery('age', 'DESC', 'M', $show_years);
4096
	}
4097
4098
	/**
4099
	 * General query on marriage ages.
4100
	 *
4101
	 * @param string[] $params
4102
	 *
4103
	 * @return string
4104
	 */
4105
	public function statsMarrAge($params = []) {
4106
		return $this->statsMarrAgeQuery(true, 'BOTH', -1, -1, $params);
4107
	}
4108
4109
	/**
4110
	 * Find the age between husband and wife.
4111
	 *
4112
	 * @param string[] $params
4113
	 *
4114
	 * @return string
4115
	 */
4116
	public function ageBetweenSpousesMF($params = []) {
4117
		return $this->ageBetweenSpousesQuery('nolist', 'DESC', $params);
4118
	}
4119
4120
	/**
4121
	 * Find the age between husband and wife.
4122
	 *
4123
	 * @param string[] $params
4124
	 *
4125
	 * @return string
4126
	 */
4127
	public function ageBetweenSpousesMFList($params = []) {
4128
		return $this->ageBetweenSpousesQuery('list', 'DESC', $params);
4129
	}
4130
4131
	/**
4132
	 * Find the age between wife and husband..
4133
	 *
4134
	 * @param string[] $params
4135
	 *
4136
	 * @return string
4137
	 */
4138
	public function ageBetweenSpousesFM($params = []) {
4139
		return $this->ageBetweenSpousesQuery('nolist', 'ASC', $params);
4140
	}
4141
4142
	/**
4143
	 * Find the age between wife and husband..
4144
	 *
4145
	 * @param string[] $params
4146
	 *
4147
	 * @return string
4148
	 */
4149
	public function ageBetweenSpousesFMList($params = []) {
4150
		return $this->ageBetweenSpousesQuery('list', 'ASC', $params);
4151
	}
4152
4153
	/**
4154
	 * General query on marriage ages.
4155
	 *
4156
	 * @return string
4157
	 */
4158
	public function topAgeOfMarriageFamily() {
4159
		return $this->ageOfMarriageQuery('name', 'DESC', ['1']);
4160
	}
4161
4162
	/**
4163
	 * General query on marriage ages.
4164
	 *
4165
	 * @return string
4166
	 */
4167
	public function topAgeOfMarriage() {
4168
		return $this->ageOfMarriageQuery('age', 'DESC', ['1']);
4169
	}
4170
4171
	/**
4172
	 * General query on marriage ages.
4173
	 *
4174
	 * @param string[] $params
4175
	 *
4176
	 * @return string
4177
	 */
4178
	public function topAgeOfMarriageFamilies($params = []) {
4179
		return $this->ageOfMarriageQuery('nolist', 'DESC', $params);
4180
	}
4181
4182
	/**
4183
	 * General query on marriage ages.
4184
	 *
4185
	 * @param string[] $params
4186
	 *
4187
	 * @return string
4188
	 */
4189
	public function topAgeOfMarriageFamiliesList($params = []) {
4190
		return $this->ageOfMarriageQuery('list', 'DESC', $params);
4191
	}
4192
4193
	/**
4194
	 * General query on marriage ages.
4195
	 *
4196
	 * @return string
4197
	 */
4198
	public function minAgeOfMarriageFamily() {
4199
		return $this->ageOfMarriageQuery('name', 'ASC', ['1']);
4200
	}
4201
4202
	/**
4203
	 * General query on marriage ages.
4204
	 *
4205
	 * @return string
4206
	 */
4207
	public function minAgeOfMarriage() {
4208
		return $this->ageOfMarriageQuery('age', 'ASC', ['1']);
4209
	}
4210
4211
	/**
4212
	 * General query on marriage ages.
4213
	 *
4214
	 * @param string[] $params
4215
	 *
4216
	 * @return string
4217
	 */
4218
	public function minAgeOfMarriageFamilies($params = []) {
4219
		return $this->ageOfMarriageQuery('nolist', 'ASC', $params);
4220
	}
4221
4222
	/**
4223
	 * General query on marriage ages.
4224
	 *
4225
	 * @param string[] $params
4226
	 *
4227
	 * @return string
4228
	 */
4229
	public function minAgeOfMarriageFamiliesList($params = []) {
4230
		return $this->ageOfMarriageQuery('list', 'ASC', $params);
4231
	}
4232
4233
	/**
4234
	 * Find the youngest mother
4235
	 *
4236
	 * @return string
4237
	 */
4238
	public function youngestMother() {
4239
		return $this->parentsQuery('full', 'ASC', 'F');
4240
	}
4241
4242
	/**
4243
	 * Find the name of the youngest mother.
4244
	 *
4245
	 * @return string
4246
	 */
4247
	public function youngestMotherName() {
4248
		return $this->parentsQuery('name', 'ASC', 'F');
4249
	}
4250
4251
	/**
4252
	 * Find the age of the youngest mother.
4253
	 *
4254
	 * @param bool $show_years
4255
	 *
4256
	 * @return string
4257
	 */
4258
	public function youngestMotherAge($show_years = false) {
4259
		return $this->parentsQuery('age', 'ASC', 'F', $show_years);
4260
	}
4261
4262
	/**
4263
	 * Find the oldest mother.
4264
	 *
4265
	 * @return string
4266
	 */
4267
	public function oldestMother() {
4268
		return $this->parentsQuery('full', 'DESC', 'F');
4269
	}
4270
4271
	/**
4272
	 * Find the name of the oldest mother.
4273
	 *
4274
	 * @return string
4275
	 */
4276
	public function oldestMotherName() {
4277
		return $this->parentsQuery('name', 'DESC', 'F');
4278
	}
4279
4280
	/**
4281
	 * Find the age of the oldest mother.
4282
	 *
4283
	 * @param bool $show_years
4284
	 *
4285
	 * @return string
4286
	 */
4287
	public function oldestMotherAge($show_years = false) {
4288
		return $this->parentsQuery('age', 'DESC', 'F', $show_years);
4289
	}
4290
4291
	/**
4292
	 * Find the youngest father.
4293
	 *
4294
	 * @return string
4295
	 */
4296
	public function youngestFather() {
4297
		return $this->parentsQuery('full', 'ASC', 'M');
4298
	}
4299
4300
	/**
4301
	 * Find the name of the youngest father.
4302
	 *
4303
	 * @return string
4304
	 */
4305
	public function youngestFatherName() {
4306
		return $this->parentsQuery('name', 'ASC', 'M');
4307
	}
4308
4309
	/**
4310
	 * Find the age of the youngest father.
4311
	 *
4312
	 * @param bool $show_years
4313
	 *
4314
	 * @return string
4315
	 */
4316
	public function youngestFatherAge($show_years = false) {
4317
		return $this->parentsQuery('age', 'ASC', 'M', $show_years);
4318
	}
4319
4320
	/**
4321
	 * Find the oldest father.
4322
	 *
4323
	 * @return string
4324
	 */
4325
	public function oldestFather() {
4326
		return $this->parentsQuery('full', 'DESC', 'M');
4327
	}
4328
4329
	/**
4330
	 * Find the name of the oldest father.
4331
	 *
4332
	 * @return string
4333
	 */
4334
	public function oldestFatherName() {
4335
		return $this->parentsQuery('name', 'DESC', 'M');
4336
	}
4337
4338
	/**
4339
	 * Find the age of the oldest father.
4340
	 *
4341
	 * @param bool $show_years
4342
	 *
4343
	 * @return string
4344
	 */
4345
	public function oldestFatherAge($show_years = false) {
4346
		return $this->parentsQuery('age', 'DESC', 'M', $show_years);
4347
	}
4348
4349
	/**
4350
	 * Number of husbands.
4351
	 *
4352
	 * @return string
4353
	 */
4354
	public function totalMarriedMales() {
4355
		$n = Database::prepare("SELECT SQL_CACHE COUNT(DISTINCT f_husb) FROM `##families` WHERE f_file=? AND f_gedcom LIKE '%\\n1 MARR%'")
4356
			->execute([$this->tree->getTreeId()])
4357
			->fetchOne();
4358
4359
		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

4359
		return I18N::number(/** @scrutinizer ignore-type */ $n);
Loading history...
4360
	}
4361
4362
	/**
4363
	 * Number of wives.
4364
	 *
4365
	 * @return string
4366
	 */
4367
	public function totalMarriedFemales() {
4368
		$n = Database::prepare("SELECT SQL_CACHE COUNT(DISTINCT f_wife) FROM `##families` WHERE f_file=? AND f_gedcom LIKE '%\\n1 MARR%'")
4369
			->execute([$this->tree->getTreeId()])
4370
			->fetchOne();
4371
4372
		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

4372
		return I18N::number(/** @scrutinizer ignore-type */ $n);
Loading history...
4373
	}
4374
4375
	/**
4376
	 * General query on family.
4377
	 *
4378
	 * @param string $type
4379
	 *
4380
	 * @return string
4381
	 */
4382
	private function familyQuery($type = 'full') {
4383
		$rows = $this->runSql(
4384
			" SELECT SQL_CACHE f_numchil AS tot, f_id AS id" .
4385
			" FROM `##families`" .
4386
			" WHERE" .
4387
			" f_file={$this->tree->getTreeId()}" .
4388
			" AND f_numchil = (" .
4389
			"  SELECT max( f_numchil )" .
4390
			"  FROM `##families`" .
4391
			"  WHERE f_file ={$this->tree->getTreeId()}" .
4392
			" )" .
4393
			" LIMIT 1"
4394
		);
4395
		if (!isset($rows[0])) {
4396
			return '';
4397
		}
4398
		$row    = $rows[0];
4399
		$family = Family::getInstance($row['id'], $this->tree);
4400
		switch ($type) {
4401
			default:
4402
			case 'full':
4403
				if ($family->canShow()) {
4404
					$result = $family->formatList();
4405
				} else {
4406
					$result = I18N::translate('This information is private and cannot be shown.');
4407
				}
4408
				break;
4409
			case 'size':
4410
				$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

4410
				$result = I18N::number(/** @scrutinizer ignore-type */ $row['tot']);
Loading history...
4411
				break;
4412
			case 'name':
4413
				$result = '<a href="' . e($family->url()) . '">' . $family->getFullName() . '</a>';
4414
				break;
4415
		}
4416
4417
		return $result;
4418
	}
4419
4420
	/**
4421
	 * General query on families.
4422
	 *
4423
	 * @param string   $type
4424
	 * @param string[] $params
4425
	 *
4426
	 * @return string
4427
	 */
4428
	private function topTenFamilyQuery($type = 'list', $params = []) {
4429
		if (isset($params[0])) {
4430
			$total = (int) $params[0];
4431
		} else {
4432
			$total = 10;
4433
		}
4434
		$rows = $this->runSql(
4435
			"SELECT SQL_CACHE f_numchil AS tot, f_id AS id" .
4436
			" FROM `##families`" .
4437
			" WHERE" .
4438
			" f_file={$this->tree->getTreeId()}" .
4439
			" ORDER BY tot DESC" .
4440
			" LIMIT " . $total
4441
		);
4442
		if (!isset($rows[0])) {
4443
			return '';
4444
		}
4445
		if (count($rows) < $total) {
4446
			$total = count($rows);
4447
		}
4448
		$top10 = [];
4449
		for ($c = 0; $c < $total; $c++) {
4450
			$family = Family::getInstance($rows[$c]['id'], $this->tree);
4451
			if ($family->canShow()) {
4452
				if ($type === 'list') {
4453
					$top10[] = '<li><a href="' . e($family->url()) . '">' . $family->getFullName() . '</a> - ' . 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

4453
					$top10[] = '<li><a href="' . e($family->url()) . '">' . $family->getFullName() . '</a> - ' . I18N::plural('%s child', '%s children', $rows[$c]['tot'], I18N::number(/** @scrutinizer ignore-type */ $rows[$c]['tot']));
Loading history...
4454
				} else {
4455
					$top10[] = '<a href="' . e($family->url()) . '">' . $family->getFullName() . '</a> - ' . I18N::plural('%s child', '%s children', $rows[$c]['tot'], I18N::number($rows[$c]['tot']));
4456
				}
4457
			}
4458
		}
4459
		if ($type === 'list') {
4460
			$top10 = implode('', $top10);
4461
		} else {
4462
			$top10 = implode('; ', $top10);
4463
		}
4464
		if (I18N::direction() === 'rtl') {
4465
			$top10 = str_replace(['[', ']', '(', ')', '+'], ['&rlm;[', '&rlm;]', '&rlm;(', '&rlm;)', '&rlm;+'], $top10);
4466
		}
4467
		if ($type === 'list') {
4468
			return '<ul>' . $top10 . '</ul>';
4469
		}
4470
4471
		return $top10;
4472
	}
4473
4474
	/**
4475
	 * Find the ages between siblings.
4476
	 *
4477
	 * @param string   $type
4478
	 * @param string[] $params
4479
	 *
4480
	 * @return string
4481
	 */
4482
	private function ageBetweenSiblingsQuery($type = 'list', $params = []) {
4483
		if (isset($params[0])) {
4484
			$total = (int) $params[0];
4485
		} else {
4486
			$total = 10;
4487
		}
4488
		if (isset($params[1])) {
4489
			$one = $params[1];
4490
		} else {
4491
			$one = false;
4492
		} // each family only once if true
4493
		$rows = $this->runSql(
4494
			" SELECT SQL_CACHE DISTINCT" .
4495
			" link1.l_from AS family," .
4496
			" link1.l_to AS ch1," .
4497
			" link2.l_to AS ch2," .
4498
			" child1.d_julianday2-child2.d_julianday2 AS age" .
4499
			" FROM `##link` AS link1" .
4500
			" LEFT JOIN `##dates` AS child1 ON child1.d_file = {$this->tree->getTreeId()}" .
4501
			" LEFT JOIN `##dates` AS child2 ON child2.d_file = {$this->tree->getTreeId()}" .
4502
			" LEFT JOIN `##link` AS link2 ON link2.l_file = {$this->tree->getTreeId()}" .
4503
			" WHERE" .
4504
			" link1.l_file = {$this->tree->getTreeId()} AND" .
4505
			" link1.l_from = link2.l_from AND" .
4506
			" link1.l_type = 'CHIL' AND" .
4507
			" child1.d_gid = link1.l_to AND" .
4508
			" child1.d_fact = 'BIRT' AND" .
4509
			" link2.l_type = 'CHIL' AND" .
4510
			" child2.d_gid = link2.l_to AND" .
4511
			" child2.d_fact = 'BIRT' AND" .
4512
			" child1.d_julianday2 > child2.d_julianday2 AND" .
4513
			" child2.d_julianday2 <> 0 AND" .
4514
			" child1.d_gid <> child2.d_gid" .
4515
			" ORDER BY age DESC" .
4516
			" LIMIT " . $total
4517
		);
4518
		if (!isset($rows[0])) {
4519
			return '';
4520
		}
4521
		$top10 = [];
4522
		$dist  = [];
4523
		foreach ($rows as $fam) {
4524
			$family = Family::getInstance($fam['family'], $this->tree);
4525
			$child1 = Individual::getInstance($fam['ch1'], $this->tree);
4526
			$child2 = Individual::getInstance($fam['ch2'], $this->tree);
4527
			if ($type == 'name') {
4528
				if ($child1->canShow() && $child2->canShow()) {
4529
					$return = '<a href="' . e($child2->url()) . '">' . $child2->getFullName() . '</a> ';
4530
					$return .= I18N::translate('and') . ' ';
4531
					$return .= '<a href="' . e($child1->url()) . '">' . $child1->getFullName() . '</a>';
4532
					$return .= ' <a href="' . e($family->url()) . '">[' . I18N::translate('View this family') . ']</a>';
4533
				} else {
4534
					$return = I18N::translate('This information is private and cannot be shown.');
4535
				}
4536
4537
				return $return;
4538
			}
4539
			$age = $fam['age'];
4540
			if ((int) ($age / 365.25) > 0) {
4541
				$age = (int) ($age / 365.25) . 'y';
4542
			} elseif ((int) ($age / 30.4375) > 0) {
4543
				$age = (int) ($age / 30.4375) . 'm';
4544
			} else {
4545
				$age = $age . 'd';
4546
			}
4547
			$age = FunctionsDate::getAgeAtEvent($age);
4548
			if ($type == 'age') {
4549
				return $age;
4550
			}
4551
			if ($type == 'list') {
4552
				if ($one && !in_array($fam['family'], $dist)) {
4553
					if ($child1->canShow() && $child2->canShow()) {
4554
						$return = '<li>';
4555
						$return .= '<a href="' . e($child2->url()) . '">' . $child2->getFullName() . '</a> ';
4556
						$return .= I18N::translate('and') . ' ';
4557
						$return .= '<a href="' . e($child1->url()) . '">' . $child1->getFullName() . '</a>';
4558
						$return .= ' (' . $age . ')';
4559
						$return .= ' <a href="' . e($family->url()) . '">[' . I18N::translate('View this family') . ']</a>';
4560
						$return .= '</li>';
4561
						$top10[] = $return;
4562
						$dist[]  = $fam['family'];
4563
					}
4564
				} elseif (!$one && $child1->canShow() && $child2->canShow()) {
4565
					$return = '<li>';
4566
					$return .= '<a href="' . e($child2->url()) . '">' . $child2->getFullName() . '</a> ';
4567
					$return .= I18N::translate('and') . ' ';
4568
					$return .= '<a href="' . e($child1->url()) . '">' . $child1->getFullName() . '</a>';
4569
					$return .= ' (' . $age . ')';
4570
					$return .= ' <a href="' . e($family->url()) . '">[' . I18N::translate('View this family') . ']</a>';
4571
					$return .= '</li>';
4572
					$top10[] = $return;
4573
				}
4574
			} else {
4575
				if ($child1->canShow() && $child2->canShow()) {
4576
					$return = $child2->formatList();
4577
					$return .= '<br>' . I18N::translate('and') . '<br>';
4578
					$return .= $child1->formatList();
4579
					$return .= '<br><a href="' . e($family->url()) . '">[' . I18N::translate('View this family') . ']</a>';
4580
4581
					return $return;
4582
				} else {
4583
					return I18N::translate('This information is private and cannot be shown.');
4584
				}
4585
			}
4586
		}
4587
		if ($type === 'list') {
4588
			$top10 = implode('', $top10);
4589
		}
4590
		if (I18N::direction() === 'rtl') {
4591
			$top10 = str_replace(['[', ']', '(', ')', '+'], ['&rlm;[', '&rlm;]', '&rlm;(', '&rlm;)', '&rlm;+'], $top10);
4592
		}
4593
		if ($type === 'list') {
4594
			return '<ul>' . $top10 . '</ul>';
4595
		}
4596
4597
		return $top10;
4598
	}
4599
4600
	/**
4601
	 * Find the month in the year of the birth of the first child.
4602
	 *
4603
	 * @param bool     $simple
4604
	 * @param bool     $sex
4605
	 * @param int      $year1
4606
	 * @param int      $year2
4607
	 * @param string[] $params
4608
	 *
4609
	 * @return string|string[][]
4610
	 */
4611
	public function monthFirstChildQuery($simple = true, $sex = false, $year1 = -1, $year2 = -1, $params = []) {
4612
		$WT_STATS_CHART_COLOR1 = Theme::theme()->parameter('distribution-chart-no-values');
4613
		$WT_STATS_CHART_COLOR2 = Theme::theme()->parameter('distribution-chart-high-values');
4614
		$WT_STATS_S_CHART_X    = Theme::theme()->parameter('stats-small-chart-x');
4615
		$WT_STATS_S_CHART_Y    = Theme::theme()->parameter('stats-small-chart-y');
4616
4617
		if ($year1 >= 0 && $year2 >= 0) {
4618
			$sql_years = " AND (d_year BETWEEN '{$year1}' AND '{$year2}')";
4619
		} else {
4620
			$sql_years = '';
4621
		}
4622
		if ($sex) {
4623
			$sql_sex1 = ', i_sex';
4624
			$sql_sex2 = " JOIN `##individuals` AS child ON child1.d_file = i_file AND child1.d_gid = child.i_id ";
4625
		} else {
4626
			$sql_sex1 = '';
4627
			$sql_sex2 = '';
4628
		}
4629
		$sql =
4630
			"SELECT SQL_CACHE d_month{$sql_sex1}, COUNT(*) AS total " .
4631
			"FROM (" .
4632
			" SELECT family{$sql_sex1}, MIN(date) AS d_date, d_month" .
4633
			" FROM (" .
4634
			"  SELECT" .
4635
			"  link1.l_from AS family," .
4636
			"  link1.l_to AS child," .
4637
			"  child1.d_julianday2 AS date," .
4638
			"  child1.d_month as d_month" .
4639
			$sql_sex1 .
4640
			"  FROM `##link` AS link1" .
4641
			"  LEFT JOIN `##dates` AS child1 ON child1.d_file = {$this->tree->getTreeId()}" .
4642
			$sql_sex2 .
4643
			"  WHERE" .
4644
			"  link1.l_file = {$this->tree->getTreeId()} AND" .
4645
			"  link1.l_type = 'CHIL' AND" .
4646
			"  child1.d_gid = link1.l_to AND" .
4647
			"  child1.d_fact = 'BIRT' AND" .
4648
			"  child1.d_month IN ('JAN', 'FEB', 'MAR', 'APR', 'MAY', 'JUN', 'JUL', 'AUG', 'SEP', 'OCT', 'NOV', 'DEC')" .
4649
			$sql_years .
4650
			"  ORDER BY date" .
4651
			" ) AS children" .
4652
			" GROUP BY family, d_month{$sql_sex1}" .
4653
			") AS first_child " .
4654
			"GROUP BY d_month";
4655
		if ($sex) {
4656
			$sql .= ', i_sex';
4657
		}
4658
		$rows = $this->runSql($sql);
4659
		if ($simple) {
4660
			if (isset($params[0]) && $params[0] != '') {
4661
				$size = strtolower($params[0]);
4662
			} else {
4663
				$size = $WT_STATS_S_CHART_X . 'x' . $WT_STATS_S_CHART_Y;
4664
			}
4665
			if (isset($params[1]) && $params[1] != '') {
4666
				$color_from = strtolower($params[1]);
4667
			} else {
4668
				$color_from = $WT_STATS_CHART_COLOR1;
4669
			}
4670
			if (isset($params[2]) && $params[2] != '') {
4671
				$color_to = strtolower($params[2]);
4672
			} else {
4673
				$color_to = $WT_STATS_CHART_COLOR2;
4674
			}
4675
			$sizes = explode('x', $size);
4676
			$tot   = 0;
4677
			foreach ($rows as $values) {
4678
				$tot += $values['total'];
4679
			}
4680
			// Beware divide by zero
4681
			if ($tot == 0) {
0 ignored issues
show
introduced by
The condition $tot == 0 can never be false.
Loading history...
4682
				return '';
4683
			}
4684
			$text   = '';
4685
			$counts = [];
4686
			foreach ($rows as $values) {
4687
				$counts[] = round(100 * $values['total'] / $tot, 0);
4688
				switch ($values['d_month']) {
4689
					default:
4690
					case 'JAN':
4691
						$values['d_month'] = 1;
4692
						break;
4693
					case 'FEB':
4694
						$values['d_month'] = 2;
4695
						break;
4696
					case 'MAR':
4697
						$values['d_month'] = 3;
4698
						break;
4699
					case 'APR':
4700
						$values['d_month'] = 4;
4701
						break;
4702
					case 'MAY':
4703
						$values['d_month'] = 5;
4704
						break;
4705
					case 'JUN':
4706
						$values['d_month'] = 6;
4707
						break;
4708
					case 'JUL':
4709
						$values['d_month'] = 7;
4710
						break;
4711
					case 'AUG':
4712
						$values['d_month'] = 8;
4713
						break;
4714
					case 'SEP':
4715
						$values['d_month'] = 9;
4716
						break;
4717
					case 'OCT':
4718
						$values['d_month'] = 10;
4719
						break;
4720
					case 'NOV':
4721
						$values['d_month'] = 11;
4722
						break;
4723
					case 'DEC':
4724
						$values['d_month'] = 12;
4725
						break;
4726
				}
4727
				$text .= I18N::translate(ucfirst(strtolower(($values['d_month'])))) . ' - ' . $values['total'] . '|';
4728
			}
4729
			$chd = $this->arrayToExtendedEncoding($counts);
4730
			$chl = substr($text, 0, -1);
4731
4732
			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') . '" />';
4733
		}
4734
4735
		return $rows;
4736
	}
4737
4738
	/**
4739
	 * Find the family with the most children.
4740
	 *
4741
	 * @return string
4742
	 */
4743
	public function largestFamily() {
4744
		return $this->familyQuery('full');
4745
	}
4746
4747
	/**
4748
	 * Find the number of children in the largest family.
4749
	 *
4750
	 * @return string
4751
	 */
4752
	public function largestFamilySize() {
4753
		return $this->familyQuery('size');
4754
	}
4755
4756
	/**
4757
	 * Find the family with the most children.
4758
	 *
4759
	 * @return string
4760
	 */
4761
	public function largestFamilyName() {
4762
		return $this->familyQuery('name');
4763
	}
4764
4765
	/**
4766
	 * The the families with the most children.
4767
	 *
4768
	 * @param string[] $params
4769
	 *
4770
	 * @return string
4771
	 */
4772
	public function topTenLargestFamily($params = []) {
4773
		return $this->topTenFamilyQuery('nolist', $params);
4774
	}
4775
4776
	/**
4777
	 * Find the families with the most children.
4778
	 *
4779
	 * @param string[] $params
4780
	 *
4781
	 * @return string
4782
	 */
4783
	public function topTenLargestFamilyList($params = []) {
4784
		return $this->topTenFamilyQuery('list', $params);
4785
	}
4786
4787
	/**
4788
	 * Create a chart of the largest families.
4789
	 *
4790
	 * @param string[] $params
4791
	 *
4792
	 * @return string
4793
	 */
4794
	public function chartLargestFamilies($params = []) {
4795
		$WT_STATS_CHART_COLOR1 = Theme::theme()->parameter('distribution-chart-no-values');
4796
		$WT_STATS_CHART_COLOR2 = Theme::theme()->parameter('distribution-chart-high-values');
4797
		$WT_STATS_L_CHART_X    = Theme::theme()->parameter('stats-large-chart-x');
4798
		$WT_STATS_S_CHART_Y    = Theme::theme()->parameter('stats-small-chart-y');
4799
4800
		if (isset($params[0]) && $params[0] != '') {
4801
			$size = strtolower($params[0]);
4802
		} else {
4803
			$size = $WT_STATS_L_CHART_X . 'x' . $WT_STATS_S_CHART_Y;
4804
		}
4805
		if (isset($params[1]) && $params[1] != '') {
4806
			$color_from = strtolower($params[1]);
4807
		} else {
4808
			$color_from = $WT_STATS_CHART_COLOR1;
4809
		}
4810
		if (isset($params[2]) && $params[2] != '') {
4811
			$color_to = strtolower($params[2]);
4812
		} else {
4813
			$color_to = $WT_STATS_CHART_COLOR2;
4814
		}
4815
		if (isset($params[3]) && $params[3] != '') {
4816
			$total = strtolower($params[3]);
4817
		} else {
4818
			$total = 10;
4819
		}
4820
		$sizes = explode('x', $size);
4821
		$total = (int) $total;
4822
		$rows  = $this->runSql(
4823
			" SELECT SQL_CACHE f_numchil AS tot, f_id AS id" .
4824
			" FROM `##families`" .
4825
			" WHERE f_file={$this->tree->getTreeId()}" .
4826
			" ORDER BY tot DESC" .
4827
			" LIMIT " . $total
4828
		);
4829
		if (!isset($rows[0])) {
4830
			return '';
4831
		}
4832
		$tot = 0;
4833
		foreach ($rows as $row) {
4834
			$tot += (int) $row['tot'];
4835
		}
4836
		$chd = '';
4837
		$chl = [];
4838
		foreach ($rows as $row) {
4839
			$family = Family::getInstance($row['id'], $this->tree);
4840
			if ($family->canShow()) {
4841
				if ($tot == 0) {
0 ignored issues
show
introduced by
The condition $tot == 0 can never be false.
Loading history...
4842
					$per = 0;
4843
				} else {
4844
					$per = round(100 * $row['tot'] / $tot, 0);
4845
				}
4846
				$chd .= $this->arrayToExtendedEncoding([$per]);
4847
				$chl[] = htmlspecialchars_decode(strip_tags($family->getFullName())) . ' - ' . I18N::number($row['tot']);
4848
			}
4849
		}
4850
		$chl = rawurlencode(implode('|', $chl));
4851
4852
		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') . '" />';
4853
	}
4854
4855
	/**
4856
	 * Count the total children.
4857
	 *
4858
	 * @return string
4859
	 */
4860
	public function totalChildren() {
4861
		$rows = $this->runSql("SELECT SQL_CACHE SUM(f_numchil) AS tot FROM `##families` WHERE f_file={$this->tree->getTreeId()}");
4862
4863
		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

4863
		return I18N::number(/** @scrutinizer ignore-type */ $rows[0]['tot']);
Loading history...
4864
	}
4865
4866
	/**
4867
	 * Find the average number of children in families.
4868
	 *
4869
	 * @return string
4870
	 */
4871
	public function averageChildren() {
4872
		$rows = $this->runSql("SELECT SQL_CACHE AVG(f_numchil) AS tot FROM `##families` WHERE f_file={$this->tree->getTreeId()}");
4873
4874
		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

4874
		return I18N::number(/** @scrutinizer ignore-type */ $rows[0]['tot'], 2);
Loading history...
4875
	}
4876
4877
	/**
4878
	 * General query on familes/children.
4879
	 *
4880
	 * @param bool     $simple
4881
	 * @param string   $sex
4882
	 * @param int      $year1
4883
	 * @param int      $year2
4884
	 * @param string[] $params
4885
	 *
4886
	 * @return string|string[][]
4887
	 */
4888
	public function statsChildrenQuery($simple = true, $sex = 'BOTH', $year1 = -1, $year2 = -1, $params = []) {
4889
		if ($simple) {
4890
			if (isset($params[0]) && $params[0] != '') {
4891
				$size = strtolower($params[0]);
4892
			} else {
4893
				$size = '220x200';
4894
			}
4895
			$sizes = explode('x', $size);
4896
			$max   = 0;
4897
			$rows  = $this->runSql(
4898
				" SELECT SQL_CACHE ROUND(AVG(f_numchil),2) AS num, FLOOR(d_year/100+1) AS century" .
4899
				" FROM  `##families`" .
4900
				" JOIN  `##dates` ON (d_file = f_file AND d_gid=f_id)" .
4901
				" WHERE f_file = {$this->tree->getTreeId()}" .
4902
				" AND   d_julianday1<>0" .
4903
				" AND   d_fact = 'MARR'" .
4904
				" AND   d_type IN ('@#DGREGORIAN@', '@#DJULIAN@')" .
4905
				" GROUP BY century" .
4906
				" ORDER BY century");
4907
			if (empty($rows)) {
4908
				return '';
4909
			}
4910
			foreach ($rows as $values) {
4911
				if ($max < $values['num']) {
4912
					$max = $values['num'];
4913
				}
4914
			}
4915
			$chm    = '';
4916
			$chxl   = '0:|';
4917
			$i      = 0;
4918
			$counts = [];
4919
			foreach ($rows as $values) {
4920
				if ($sizes[0] < 980) {
4921
					$sizes[0] += 38;
4922
				}
4923
				$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

4923
				$chxl .= $this->centuryName(/** @scrutinizer ignore-type */ $values['century']) . '|';
Loading history...
4924
				if ($max <= 5) {
4925
					$counts[] = round($values['num'] * 819.2 - 1, 1);
4926
				} elseif ($max <= 10) {
4927
					$counts[] = round($values['num'] * 409.6, 1);
4928
				} else {
4929
					$counts[] = round($values['num'] * 204.8, 1);
4930
				}
4931
				$chm .= 't' . $values['num'] . ',000000,0,' . $i . ',11,1|';
4932
				$i++;
4933
			}
4934
			$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

4934
			$chd = $this->arrayToExtendedEncoding(/** @scrutinizer ignore-type */ $counts);
Loading history...
4935
			$chm = substr($chm, 0, -1);
4936
			if ($max <= 5) {
4937
				$chxl .= '1:||' . I18N::translate('century') . '|2:|0|1|2|3|4|5|3:||' . I18N::translate('Number of children') . '|';
4938
			} elseif ($max <= 10) {
4939
				$chxl .= '1:||' . I18N::translate('century') . '|2:|0|1|2|3|4|5|6|7|8|9|10|3:||' . I18N::translate('Number of children') . '|';
4940
			} else {
4941
				$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') . '|';
4942
			}
4943
4944
			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') . '" />';
4945
		} else {
4946
			if ($sex == 'M') {
4947
				$sql =
4948
					"SELECT SQL_CACHE num, COUNT(*) AS total FROM " .
4949
					"(SELECT count(i_sex) AS num FROM `##link` " .
4950
					"LEFT OUTER JOIN `##individuals` " .
4951
					"ON l_from=i_id AND l_file=i_file AND i_sex='M' AND l_type='FAMC' " .
4952
					"JOIN `##families` ON f_file=l_file AND f_id=l_to WHERE f_file={$this->tree->getTreeId()} GROUP BY l_to" .
4953
					") boys" .
4954
					" GROUP BY num" .
4955
					" ORDER BY num";
4956
			} elseif ($sex == 'F') {
4957
				$sql =
4958
					"SELECT SQL_CACHE num, COUNT(*) AS total FROM " .
4959
					"(SELECT count(i_sex) AS num FROM `##link` " .
4960
					"LEFT OUTER JOIN `##individuals` " .
4961
					"ON l_from=i_id AND l_file=i_file AND i_sex='F' AND l_type='FAMC' " .
4962
					"JOIN `##families` ON f_file=l_file AND f_id=l_to WHERE f_file={$this->tree->getTreeId()} GROUP BY l_to" .
4963
					") girls" .
4964
					" GROUP BY num" .
4965
					" ORDER BY num";
4966
			} else {
4967
				$sql = "SELECT SQL_CACHE f_numchil, COUNT(*) AS total FROM `##families` ";
4968
				if ($year1 >= 0 && $year2 >= 0) {
4969
					$sql .=
4970
						"AS fam LEFT JOIN `##dates` AS married ON married.d_file = {$this->tree->getTreeId()}"
4971
						. " WHERE"
4972
						. " married.d_gid = fam.f_id AND"
4973
						. " fam.f_file = {$this->tree->getTreeId()} AND"
4974
						. " married.d_fact = 'MARR' AND"
4975
						. " married.d_year BETWEEN '{$year1}' AND '{$year2}'";
4976
				} else {
4977
					$sql .= "WHERE f_file={$this->tree->getTreeId()}";
4978
				}
4979
				$sql .= ' GROUP BY f_numchil';
4980
			}
4981
			$rows = $this->runSql($sql);
4982
4983
			return $rows;
4984
		}
4985
	}
4986
4987
	/**
4988
	 * Genearl query on families/children.
4989
	 *
4990
	 * @param string[] $params
4991
	 *
4992
	 * @return string
4993
	 */
4994
	public function statsChildren($params = []) {
4995
		return $this->statsChildrenQuery(true, 'BOTH', -1, -1, $params);
4996
	}
4997
4998
	/**
4999
	 * Find the names of siblings with the widest age gap.
5000
	 *
5001
	 * @param string[] $params
5002
	 *
5003
	 * @return string
5004
	 */
5005
	public function topAgeBetweenSiblingsName($params = []) {
5006
		return $this->ageBetweenSiblingsQuery('name', $params);
5007
	}
5008
5009
	/**
5010
	 * Find the widest age gap between siblings.
5011
	 *
5012
	 * @param string[] $params
5013
	 *
5014
	 * @return string
5015
	 */
5016
	public function topAgeBetweenSiblings($params = []) {
5017
		return $this->ageBetweenSiblingsQuery('age', $params);
5018
	}
5019
5020
	/**
5021
	 * Find the name of siblings with the widest age gap.
5022
	 *
5023
	 * @param string[] $params
5024
	 *
5025
	 * @return string
5026
	 */
5027
	public function topAgeBetweenSiblingsFullName($params = []) {
5028
		return $this->ageBetweenSiblingsQuery('nolist', $params);
5029
	}
5030
5031
	/**
5032
	 * Find the siblings with the widest age gaps.
5033
	 *
5034
	 * @param string[] $params
5035
	 *
5036
	 * @return string
5037
	 */
5038
	public function topAgeBetweenSiblingsList($params = []) {
5039
		return $this->ageBetweenSiblingsQuery('list', $params);
5040
	}
5041
5042
	/**
5043
	 * Find the families with no children.
5044
	 *
5045
	 * @return string
5046
	 */
5047
	private function noChildrenFamiliesQuery() {
5048
		$rows = $this->runSql(
5049
			" SELECT SQL_CACHE COUNT(*) AS tot" .
5050
			" FROM  `##families`" .
5051
			" WHERE f_numchil = 0 AND f_file = {$this->tree->getTreeId()}");
5052
5053
		return $rows[0]['tot'];
5054
	}
5055
5056
	/**
5057
	 * Find the families with no children.
5058
	 *
5059
	 * @return string
5060
	 */
5061
	public function noChildrenFamilies() {
5062
		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

5062
		return I18N::number(/** @scrutinizer ignore-type */ $this->noChildrenFamiliesQuery());
Loading history...
5063
	}
5064
5065
	/**
5066
	 * Find the families with no children.
5067
	 *
5068
	 * @param string[] $params
5069
	 *
5070
	 * @return string
5071
	 */
5072
	public function noChildrenFamiliesList($params = []) {
5073
		if (isset($params[0]) && $params[0] != '') {
5074
			$type = strtolower($params[0]);
5075
		} else {
5076
			$type = 'list';
5077
		}
5078
		$rows = $this->runSql(
5079
			" SELECT SQL_CACHE f_id AS family" .
5080
			" FROM `##families` AS fam" .
5081
			" WHERE f_numchil = 0 AND fam.f_file = {$this->tree->getTreeId()}");
5082
		if (!isset($rows[0])) {
5083
			return '';
5084
		}
5085
		$top10 = [];
5086
		foreach ($rows as $row) {
5087
			$family = Family::getInstance($row['family'], $this->tree);
5088
			if ($family->canShow()) {
5089
				if ($type == 'list') {
5090
					$top10[] = '<li><a href="' . e($family->url()) . '">' . $family->getFullName() . '</a></li>';
5091
				} else {
5092
					$top10[] = '<a href="' . e($family->url()) . '">' . $family->getFullName() . '</a>';
5093
				}
5094
			}
5095
		}
5096
		if ($type == 'list') {
5097
			$top10 = implode('', $top10);
5098
		} else {
5099
			$top10 = implode('; ', $top10);
5100
		}
5101
		if (I18N::direction() === 'rtl') {
5102
			$top10 = str_replace(['[', ']', '(', ')', '+'], ['&rlm;[', '&rlm;]', '&rlm;(', '&rlm;)', '&rlm;+'], $top10);
5103
		}
5104
		if ($type === 'list') {
5105
			return '<ul>' . $top10 . '</ul>';
5106
		}
5107
5108
		return $top10;
5109
	}
5110
5111
	/**
5112
	 * Create a chart of children with no families.
5113
	 *
5114
	 * @param string[] $params
5115
	 *
5116
	 * @return string
5117
	 */
5118
	public function chartNoChildrenFamilies($params = []) {
5119
		if (isset($params[0]) && $params[0] != '') {
5120
			$size = strtolower($params[0]);
5121
		} else {
5122
			$size = '220x200';
5123
		}
5124
		if (isset($params[1]) && $params[1] != '') {
5125
			$year1 = $params[1];
5126
		} else {
5127
			$year1 = -1;
5128
		}
5129
		if (isset($params[2]) && $params[2] != '') {
5130
			$year2 = $params[2];
5131
		} else {
5132
			$year2 = -1;
5133
		}
5134
		$sizes = explode('x', $size);
5135
		if ($year1 >= 0 && $year2 >= 0) {
5136
			$years = " married.d_year BETWEEN '{$year1}' AND '{$year2}' AND";
5137
		} else {
5138
			$years = '';
5139
		}
5140
		$max  = 0;
5141
		$tot  = 0;
5142
		$rows = $this->runSql(
5143
			"SELECT SQL_CACHE" .
5144
			" COUNT(*) AS count," .
5145
			" FLOOR(married.d_year/100+1) AS century" .
5146
			" FROM" .
5147
			" `##families` AS fam" .
5148
			" JOIN" .
5149
			" `##dates` AS married ON (married.d_file = fam.f_file AND married.d_gid = fam.f_id)" .
5150
			" WHERE" .
5151
			" f_numchil = 0 AND" .
5152
			" fam.f_file = {$this->tree->getTreeId()} AND" .
5153
			$years .
5154
			" married.d_fact = 'MARR' AND" .
5155
			" married.d_type IN ('@#DGREGORIAN@', '@#DJULIAN@')" .
5156
			" GROUP BY century ORDER BY century"
5157
		);
5158
		if (empty($rows)) {
5159
			return '';
5160
		}
5161
		foreach ($rows as $values) {
5162
			if ($max < $values['count']) {
5163
				$max = $values['count'];
5164
			}
5165
			$tot += (int) $values['count'];
5166
		}
5167
		$unknown = $this->noChildrenFamiliesQuery() - $tot;
5168
		if ($unknown > $max) {
5169
			$max = $unknown;
5170
		}
5171
		$chm    = '';
5172
		$chxl   = '0:|';
5173
		$i      = 0;
5174
		$counts = [];
5175
		foreach ($rows as $values) {
5176
			if ($sizes[0] < 980) {
5177
				$sizes[0] += 38;
5178
			}
5179
			$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

5179
			$chxl .= $this->centuryName(/** @scrutinizer ignore-type */ $values['century']) . '|';
Loading history...
5180
			$counts[] = round(4095 * $values['count'] / ($max + 1));
5181
			$chm .= 't' . $values['count'] . ',000000,0,' . $i . ',11,1|';
5182
			$i++;
5183
		}
5184
		$counts[] = round(4095 * $unknown / ($max + 1));
5185
		$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

5185
		$chd      = $this->arrayToExtendedEncoding(/** @scrutinizer ignore-type */ $counts);
Loading history...
5186
		$chm .= 't' . $unknown . ',000000,0,' . $i . ',11,1';
5187
		$chxl .= I18N::translateContext('unknown century', 'Unknown') . '|1:||' . I18N::translate('century') . '|2:|0|';
5188
		$step = $max + 1;
5189
		for ($d = (int) ($max + 1); $d > 0; $d--) {
5190
			if (($max + 1) < ($d * 10 + 1) && fmod(($max + 1), $d) == 0) {
5191
				$step = $d;
5192
			}
5193
		}
5194
		if ($step == (int) ($max + 1)) {
5195
			for ($d = (int) ($max); $d > 0; $d--) {
5196
				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 $x 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

5196
				if ($max < ($d * 10 + 1) && fmod(/** @scrutinizer ignore-type */ $max, $d) == 0) {
Loading history...
5197
					$step = $d;
5198
				}
5199
			}
5200
		}
5201
		for ($n = $step; $n <= ($max + 1); $n += $step) {
5202
			$chxl .= $n . '|';
5203
		}
5204
		$chxl .= '3:||' . I18N::translate('Total families') . '|';
5205
5206
		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') . '" />';
5207
	}
5208
5209
	/**
5210
	 * Find the couple with the most grandchildren.
5211
	 *
5212
	 * @param string   $type
5213
	 * @param string[] $params
5214
	 *
5215
	 * @return string
5216
	 */
5217
	private function topTenGrandFamilyQuery($type = 'list', $params = []) {
5218
		if (isset($params[0])) {
5219
			$total = (int) $params[0];
5220
		} else {
5221
			$total = 10;
5222
		}
5223
		$rows = $this->runSql(
5224
			"SELECT SQL_CACHE COUNT(*) AS tot, f_id AS id" .
5225
			" FROM `##families`" .
5226
			" JOIN `##link` AS children ON children.l_file = {$this->tree->getTreeId()}" .
5227
			" JOIN `##link` AS mchildren ON mchildren.l_file = {$this->tree->getTreeId()}" .
5228
			" JOIN `##link` AS gchildren ON gchildren.l_file = {$this->tree->getTreeId()}" .
5229
			" WHERE" .
5230
			" f_file={$this->tree->getTreeId()} AND" .
5231
			" children.l_from=f_id AND" .
5232
			" children.l_type='CHIL' AND" .
5233
			" children.l_to=mchildren.l_from AND" .
5234
			" mchildren.l_type='FAMS' AND" .
5235
			" mchildren.l_to=gchildren.l_from AND" .
5236
			" gchildren.l_type='CHIL'" .
5237
			" GROUP BY id" .
5238
			" ORDER BY tot DESC" .
5239
			" LIMIT " . $total
5240
		);
5241
		if (!isset($rows[0])) {
5242
			return '';
5243
		}
5244
		$top10 = [];
5245
		foreach ($rows as $row) {
5246
			$family = Family::getInstance($row['id'], $this->tree);
5247
			if ($family->canShow()) {
5248
				if ($type === 'list') {
5249
					$top10[] = '<li><a href="' . e($family->url()) . '">' . $family->getFullName() . '</a> - ' . 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

5249
					$top10[] = '<li><a href="' . e($family->url()) . '">' . $family->getFullName() . '</a> - ' . I18N::plural('%s grandchild', '%s grandchildren', $row['tot'], I18N::number(/** @scrutinizer ignore-type */ $row['tot']));
Loading history...
5250
				} else {
5251
					$top10[] = '<a href="' . e($family->url()) . '">' . $family->getFullName() . '</a> - ' . I18N::plural('%s grandchild', '%s grandchildren', $row['tot'], I18N::number($row['tot']));
5252
				}
5253
			}
5254
		}
5255
		if ($type === 'list') {
5256
			$top10 = implode('', $top10);
5257
		} else {
5258
			$top10 = implode('; ', $top10);
5259
		}
5260
		if (I18N::direction() === 'rtl') {
5261
			$top10 = str_replace(['[', ']', '(', ')', '+'], ['&rlm;[', '&rlm;]', '&rlm;(', '&rlm;)', '&rlm;+'], $top10);
5262
		}
5263
		if ($type === 'list') {
5264
			return '<ul>' . $top10 . '</ul>';
5265
		}
5266
5267
		return $top10;
5268
	}
5269
5270
	/**
5271
	 * Find the couple with the most grandchildren.
5272
	 *
5273
	 * @param string[] $params
5274
	 *
5275
	 * @return string
5276
	 */
5277
	public function topTenLargestGrandFamily($params = []) {
5278
		return $this->topTenGrandFamilyQuery('nolist', $params);
5279
	}
5280
5281
	/**
5282
	 * Find the couple with the most grandchildren.
5283
	 *
5284
	 * @param string[] $params
5285
	 *
5286
	 * @return string
5287
	 */
5288
	public function topTenLargestGrandFamilyList($params = []) {
5289
		return $this->topTenGrandFamilyQuery('list', $params);
5290
	}
5291
5292
	/**
5293
	 * Find common surnames.
5294
	 *
5295
	 * @param string   $type
5296
	 * @param bool     $show_tot
5297
	 * @param string[] $params
5298
	 *
5299
	 * @return string
5300
	 */
5301
	private function commonSurnamesQuery($type = 'list', $show_tot = false, $params = []) {
5302
		$threshold          = empty($params[0]) ? 10 : (int) $params[0];
5303
		$number_of_surnames = empty($params[1]) ? 10 : (int) $params[1];
5304
		$sorting            = empty($params[2]) ? 'alpha' : $params[2];
5305
5306
		$surname_list = FunctionsDb::getTopSurnames($this->tree->getTreeId(), $threshold, $number_of_surnames);
5307
		if (empty($surname_list)) {
5308
			return '';
5309
		}
5310
5311
		switch ($sorting) {
5312
			default:
5313
			case 'alpha':
5314
				uksort($surname_list, '\Fisharebest\Webtrees\I18N::strcasecmp');
5315
				break;
5316
			case 'count':
5317
				asort($surname_list);
5318
				break;
5319
			case 'rcount':
5320
				arsort($surname_list);
5321
				break;
5322
		}
5323
5324
		// Note that we count/display SPFX SURN, but sort/group under just SURN
5325
		$surnames = [];
5326
		foreach (array_keys($surname_list) as $surname) {
5327
			$surnames = array_merge($surnames, QueryName::surnames($this->tree, $surname, '', false, false));
5328
		}
5329
5330
		return FunctionsPrintLists::surnameList($surnames, ($type == 'list' ? 1 : 2), $show_tot, 'indilist.php', $this->tree);
5331
	}
5332
5333
	/**
5334
	 * Find common surnames.
5335
	 *
5336
	 * @return string
5337
	 */
5338
	public function getCommonSurname() {
5339
		$surnames = array_keys(FunctionsDb::getTopSurnames($this->tree->getTreeId(), 1, 1));
5340
5341
		return array_shift($surnames);
5342
	}
5343
5344
	/**
5345
	 * Find common surnames.
5346
	 *
5347
	 * @param string[] $params
5348
	 *
5349
	 * @return string
5350
	 */
5351
	public function commonSurnames($params = ['', '', 'alpha']) {
5352
		return $this->commonSurnamesQuery('nolist', false, $params);
5353
	}
5354
5355
	/**
5356
	 * Find common surnames.
5357
	 *
5358
	 * @param string[] $params
5359
	 *
5360
	 * @return string
5361
	 */
5362
	public function commonSurnamesTotals($params = ['', '', 'rcount']) {
5363
		return $this->commonSurnamesQuery('nolist', true, $params);
5364
	}
5365
5366
	/**
5367
	 * Find common surnames.
5368
	 *
5369
	 * @param string[] $params
5370
	 *
5371
	 * @return string
5372
	 */
5373
	public function commonSurnamesList($params = ['', '', 'alpha']) {
5374
		return $this->commonSurnamesQuery('list', false, $params);
5375
	}
5376
5377
	/**
5378
	 * Find common surnames.
5379
	 *
5380
	 * @param string[] $params
5381
	 *
5382
	 * @return string
5383
	 */
5384
	public function commonSurnamesListTotals($params = ['', '', 'rcount']) {
5385
		return $this->commonSurnamesQuery('list', true, $params);
5386
	}
5387
5388
	/**
5389
	 * Create a chart of common surnames.
5390
	 *
5391
	 * @param string[] $params
5392
	 *
5393
	 * @return string
5394
	 */
5395
	public function chartCommonSurnames($params = []) {
5396
		$WT_STATS_CHART_COLOR1 = Theme::theme()->parameter('distribution-chart-no-values');
5397
		$WT_STATS_CHART_COLOR2 = Theme::theme()->parameter('distribution-chart-high-values');
5398
		$WT_STATS_S_CHART_X    = Theme::theme()->parameter('stats-small-chart-x');
5399
		$WT_STATS_S_CHART_Y    = Theme::theme()->parameter('stats-small-chart-y');
5400
5401
		$size               = empty($params[0]) ? $WT_STATS_S_CHART_X . 'x' . $WT_STATS_S_CHART_Y : strtolower($params[0]);
5402
		$color_from         = empty($params[1]) ? $WT_STATS_CHART_COLOR1 : strtolower($params[1]);
5403
		$color_to           = empty($params[2]) ? $WT_STATS_CHART_COLOR2 : strtolower($params[2]);
5404
		$number_of_surnames = empty($params[3]) ? 10 : (int) $params[3];
5405
5406
		$sizes    = explode('x', $size);
5407
		$tot_indi = $this->totalIndividualsQuery();
5408
		$surnames = FunctionsDb::getTopSurnames($this->tree->getTreeId(), 0, $number_of_surnames);
5409
		if (empty($surnames)) {
5410
			return '';
5411
		}
5412
		$SURNAME_TRADITION = $this->tree->getPreference('SURNAME_TRADITION');
5413
		$all_surnames      = [];
5414
		$tot               = 0;
5415
		foreach ($surnames as $surname => $num) {
5416
			$all_surnames = array_merge($all_surnames, QueryName::surnames($this->tree, I18N::strtoupper($surname), '', false, false));
5417
			$tot += $num;
5418
		}
5419
		$chd = '';
5420
		$chl = [];
5421
		foreach ($all_surnames as $surns) {
5422
			$count_per = 0;
5423
			$max_name  = 0;
5424
			$top_name  = '';
5425
			foreach ($surns as $spfxsurn => $indis) {
5426
				$per = count($indis);
5427
				$count_per += $per;
5428
				// select most common surname from all variants
5429
				if ($per > $max_name) {
5430
					$max_name = $per;
5431
					$top_name = $spfxsurn;
5432
				}
5433
			}
5434
			switch ($SURNAME_TRADITION) {
5435
				case 'polish':
5436
					// most common surname should be in male variant (Kowalski, not Kowalska)
5437
					$top_name = preg_replace(['/ska$/', '/cka$/', '/dzka$/', '/żka$/'], ['ski', 'cki', 'dzki', 'żki'], $top_name);
5438
			}
5439
			$per = round(100 * $count_per / $tot_indi, 0);
5440
			$chd .= $this->arrayToExtendedEncoding([$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

5440
			$chd .= $this->arrayToExtendedEncoding(/** @scrutinizer ignore-type */ [$per]);
Loading history...
5441
			$chl[] = $top_name . ' - ' . I18N::number($count_per);
5442
		}
5443
		$per = round(100 * ($tot_indi - $tot) / $tot_indi, 0);
5444
		$chd .= $this->arrayToExtendedEncoding([$per]);
5445
		$chl[] = I18N::translate('Other') . ' - ' . I18N::number($tot_indi - $tot);
5446
5447
		$chart_title = implode(I18N::$list_separator, $chl);
5448
		$chl         = implode('|', $chl);
5449
5450
		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 . '" />';
5451
	}
5452
5453
	/**
5454
	 * Find common given names.
5455
	 *
5456
	 * @param string   $sex
5457
	 * @param string   $type
5458
	 * @param bool     $show_tot
5459
	 * @param string[] $params
5460
	 *
5461
	 * @return string
5462
	 */
5463
	private function commonGivenQuery($sex = 'B', $type = 'list', $show_tot = false, $params = []) {
5464
		if (isset($params[0]) && $params[0] != '' && $params[0] >= 0) {
5465
			$threshold = (int) $params[0];
5466
		} else {
5467
			$threshold = 1;
5468
		}
5469
		if (isset($params[1]) && $params[1] != '' && $params[1] >= 0) {
5470
			$maxtoshow = (int) $params[1];
5471
		} else {
5472
			$maxtoshow = 10;
5473
		}
5474
5475
		switch ($sex) {
5476
			case 'M':
5477
				$sex_sql = "i_sex='M'";
5478
				break;
5479
			case 'F':
5480
				$sex_sql = "i_sex='F'";
5481
				break;
5482
			case 'U':
5483
				$sex_sql = "i_sex='U'";
5484
				break;
5485
			case 'B':
5486
			default:
5487
				$sex_sql = "i_sex<>'U'";
5488
				break;
5489
		}
5490
		$ged_id = $this->tree->getTreeId();
5491
5492
		$rows = Database::prepare("SELECT SQL_CACHE 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")
5493
			->fetchAll();
5494
		$nameList = [];
5495
		foreach ($rows as $row) {
5496
			// Split “John Thomas” into “John” and “Thomas” and count against both totals
5497
			foreach (explode(' ', $row->n_givn) as $given) {
5498
				// Exclude initials and particles.
5499
				if (!preg_match('/^([A-Z]|[a-z]{1,3})$/', $given)) {
5500
					if (array_key_exists($given, $nameList)) {
5501
						$nameList[$given] += $row->num;
5502
					} else {
5503
						$nameList[$given] = $row->num;
5504
					}
5505
				}
5506
			}
5507
		}
5508
		arsort($nameList, SORT_NUMERIC);
5509
		$nameList = array_slice($nameList, 0, $maxtoshow);
5510
5511
		if (count($nameList) == 0) {
5512
			return '';
5513
		}
5514
		if ($type == 'chart') {
5515
			return $nameList;
5516
		}
5517
		$common = [];
5518
		foreach ($nameList as $given => $total) {
5519
			if ($maxtoshow !== -1) {
5520
				if ($maxtoshow-- <= 0) {
5521
					break;
5522
				}
5523
			}
5524
			if ($total < $threshold) {
5525
				break;
5526
			}
5527
			if ($show_tot) {
5528
				$tot = ' (' . I18N::number($total) . ')';
5529
			} else {
5530
				$tot = '';
5531
			}
5532
			switch ($type) {
5533
				case 'table':
5534
					$common[] = '<tr><td>' . $given . '</td><td data-sort="' . $total . '">' . I18N::number($total) . '</td></tr>';
5535
					break;
5536
				case 'list':
5537
					$common[] = '<li><span dir="auto">' . $given . '</span>' . $tot . '</li>';
5538
					break;
5539
				case 'nolist':
5540
					$common[] = '<span dir="auto">' . $given . '</span>' . $tot;
5541
					break;
5542
			}
5543
		}
5544
		if ($common) {
0 ignored issues
show
introduced by
The condition $common can never be true.
Loading history...
5545
			switch ($type) {
5546
				case 'table':
5547
					$lookup = ['M' => I18N::translate('Male'), 'F' => I18N::translate('Female'), 'U' => I18N::translateContext('unknown gender', 'Unknown'), 'B' => I18N::translate('All')];
5548
5549
					return '<table ' . Datatables::givenNameTableAttributes() . '><thead><tr><th colspan="3">' . $lookup[$sex] . '</th></tr><tr><th>' . I18N::translate('Name') . '</th><th>' . I18N::translate('Individuals') . '</th></tr></thead><tbody>' . implode('', $common) . '</tbody></table>';
5550
				case 'list':
5551
					return '<ul>' . implode('', $common) . '</ul>';
5552
				case 'nolist':
5553
					return implode(I18N::$list_separator, $common);
5554
				default:
5555
					return '';
5556
			}
5557
		} else {
5558
			return '';
5559
		}
5560
	}
5561
5562
	/**
5563
	 * Find common give names.
5564
	 *
5565
	 * @param string[] $params
5566
	 *
5567
	 * @return string
5568
	 */
5569
	public function commonGiven($params = [1, 10, 'alpha']) {
5570
		return $this->commonGivenQuery('B', 'nolist', false, $params);
5571
	}
5572
5573
	/**
5574
	 * Find common give names.
5575
	 *
5576
	 * @param string[] $params
5577
	 *
5578
	 * @return string
5579
	 */
5580
	public function commonGivenTotals($params = [1, 10, 'rcount']) {
5581
		return $this->commonGivenQuery('B', 'nolist', true, $params);
5582
	}
5583
5584
	/**
5585
	 * Find common give names.
5586
	 *
5587
	 * @param string[] $params
5588
	 *
5589
	 * @return string
5590
	 */
5591
	public function commonGivenList($params = [1, 10, 'alpha']) {
5592
		return $this->commonGivenQuery('B', 'list', false, $params);
5593
	}
5594
5595
	/**
5596
	 * Find common give names.
5597
	 *
5598
	 * @param string[] $params
5599
	 *
5600
	 * @return string
5601
	 */
5602
	public function commonGivenListTotals($params = [1, 10, 'rcount']) {
5603
		return $this->commonGivenQuery('B', 'list', true, $params);
5604
	}
5605
5606
	/**
5607
	 * Find common give names.
5608
	 *
5609
	 * @param string[] $params
5610
	 *
5611
	 * @return string
5612
	 */
5613
	public function commonGivenTable($params = [1, 10, 'rcount']) {
5614
		return $this->commonGivenQuery('B', 'table', false, $params);
5615
	}
5616
5617
	/**
5618
	 * Find common give names of females.
5619
	 *
5620
	 * @param string[] $params
5621
	 *
5622
	 * @return string
5623
	 */
5624
	public function commonGivenFemale($params = [1, 10, 'alpha']) {
5625
		return $this->commonGivenQuery('F', 'nolist', false, $params);
5626
	}
5627
5628
	/**
5629
	 * Find common give names of females.
5630
	 *
5631
	 * @param string[] $params
5632
	 *
5633
	 * @return string
5634
	 */
5635
	public function commonGivenFemaleTotals($params = [1, 10, 'rcount']) {
5636
		return $this->commonGivenQuery('F', 'nolist', true, $params);
5637
	}
5638
5639
	/**
5640
	 * Find common give names of females.
5641
	 *
5642
	 * @param string[] $params
5643
	 *
5644
	 * @return string
5645
	 */
5646
	public function commonGivenFemaleList($params = [1, 10, 'alpha']) {
5647
		return $this->commonGivenQuery('F', 'list', false, $params);
5648
	}
5649
5650
	/**
5651
	 * Find common give names of females.
5652
	 *
5653
	 * @param string[] $params
5654
	 *
5655
	 * @return string
5656
	 */
5657
	public function commonGivenFemaleListTotals($params = [1, 10, 'rcount']) {
5658
		return $this->commonGivenQuery('F', 'list', true, $params);
5659
	}
5660
5661
	/**
5662
	 * Find common give names of females.
5663
	 *
5664
	 * @param string[] $params
5665
	 *
5666
	 * @return string
5667
	 */
5668
	public function commonGivenFemaleTable($params = [1, 10, 'rcount']) {
5669
		return $this->commonGivenQuery('F', 'table', false, $params);
5670
	}
5671
5672
	/**
5673
	 * Find common give names of males.
5674
	 *
5675
	 * @param string[] $params
5676
	 *
5677
	 * @return string
5678
	 */
5679
	public function commonGivenMale($params = [1, 10, 'alpha']) {
5680
		return $this->commonGivenQuery('M', 'nolist', false, $params);
5681
	}
5682
5683
	/**
5684
	 * Find common give names of males.
5685
	 *
5686
	 * @param string[] $params
5687
	 *
5688
	 * @return string
5689
	 */
5690
	public function commonGivenMaleTotals($params = [1, 10, 'rcount']) {
5691
		return $this->commonGivenQuery('M', 'nolist', true, $params);
5692
	}
5693
5694
	/**
5695
	 * Find common give names of males.
5696
	 *
5697
	 * @param string[] $params
5698
	 *
5699
	 * @return string
5700
	 */
5701
	public function commonGivenMaleList($params = [1, 10, 'alpha']) {
5702
		return $this->commonGivenQuery('M', 'list', false, $params);
5703
	}
5704
5705
	/**
5706
	 * Find common give names of males.
5707
	 *
5708
	 * @param string[] $params
5709
	 *
5710
	 * @return string
5711
	 */
5712
	public function commonGivenMaleListTotals($params = [1, 10, 'rcount']) {
5713
		return $this->commonGivenQuery('M', 'list', true, $params);
5714
	}
5715
5716
	/**
5717
	 * Find common give names of males.
5718
	 *
5719
	 * @param string[] $params
5720
	 *
5721
	 * @return string
5722
	 */
5723
	public function commonGivenMaleTable($params = [1, 10, 'rcount']) {
5724
		return $this->commonGivenQuery('M', 'table', false, $params);
5725
	}
5726
5727
	/**
5728
	 * Find common give names of unknown sexes.
5729
	 *
5730
	 * @param string[] $params
5731
	 *
5732
	 * @return string
5733
	 */
5734
	public function commonGivenUnknown($params = [1, 10, 'alpha']) {
5735
		return $this->commonGivenQuery('U', 'nolist', false, $params);
5736
	}
5737
5738
	/**
5739
	 * Find common give names of unknown sexes.
5740
	 *
5741
	 * @param string[] $params
5742
	 *
5743
	 * @return string
5744
	 */
5745
	public function commonGivenUnknownTotals($params = [1, 10, 'rcount']) {
5746
		return $this->commonGivenQuery('U', 'nolist', true, $params);
5747
	}
5748
5749
	/**
5750
	 * Find common give names of unknown sexes.
5751
	 *
5752
	 * @param string[] $params
5753
	 *
5754
	 * @return string
5755
	 */
5756
	public function commonGivenUnknownList($params = [1, 10, 'alpha']) {
5757
		return $this->commonGivenQuery('U', 'list', false, $params);
5758
	}
5759
5760
	/**
5761
	 * Find common give names of unknown sexes.
5762
	 *
5763
	 * @param string[] $params
5764
	 *
5765
	 * @return string
5766
	 */
5767
	public function commonGivenUnknownListTotals($params = [1, 10, 'rcount']) {
5768
		return $this->commonGivenQuery('U', 'list', true, $params);
5769
	}
5770
5771
	/**
5772
	 * Find common give names of unknown sexes.
5773
	 *
5774
	 * @param string[] $params
5775
	 *
5776
	 * @return string
5777
	 */
5778
	public function commonGivenUnknownTable($params = [1, 10, 'rcount']) {
5779
		return $this->commonGivenQuery('U', 'table', false, $params);
5780
	}
5781
5782
	/**
5783
	 * Create a chart of common given names.
5784
	 *
5785
	 * @param string[] $params
5786
	 *
5787
	 * @return string
5788
	 */
5789
	public function chartCommonGiven($params = []) {
5790
		$WT_STATS_CHART_COLOR1 = Theme::theme()->parameter('distribution-chart-no-values');
5791
		$WT_STATS_CHART_COLOR2 = Theme::theme()->parameter('distribution-chart-high-values');
5792
		$WT_STATS_S_CHART_X    = Theme::theme()->parameter('stats-small-chart-x');
5793
		$WT_STATS_S_CHART_Y    = Theme::theme()->parameter('stats-small-chart-y');
5794
5795
		if (isset($params[0]) && $params[0] != '') {
5796
			$size = strtolower($params[0]);
5797
		} else {
5798
			$size = $WT_STATS_S_CHART_X . 'x' . $WT_STATS_S_CHART_Y;
5799
		}
5800
		if (isset($params[1]) && $params[1] != '') {
5801
			$color_from = strtolower($params[1]);
5802
		} else {
5803
			$color_from = $WT_STATS_CHART_COLOR1;
5804
		}
5805
		if (isset($params[2]) && $params[2] != '') {
5806
			$color_to = strtolower($params[2]);
5807
		} else {
5808
			$color_to = $WT_STATS_CHART_COLOR2;
5809
		}
5810
		if (isset($params[4]) && $params[4] != '') {
5811
			$maxtoshow = strtolower($params[4]);
5812
		} else {
5813
			$maxtoshow = 7;
5814
		}
5815
		$sizes    = explode('x', $size);
5816
		$tot_indi = $this->totalIndividualsQuery();
5817
		$given    = $this->commonGivenQuery('B', 'chart');
5818
		if (!is_array($given)) {
0 ignored issues
show
introduced by
The condition ! is_array($given) can never be false.
Loading history...
5819
			return '';
5820
		}
5821
		$given = array_slice($given, 0, $maxtoshow);
5822
		if (count($given) <= 0) {
5823
			return '';
5824
		}
5825
		$tot = 0;
5826
		foreach ($given as $count) {
5827
			$tot += $count;
5828
		}
5829
		$chd = '';
5830
		$chl = [];
5831
		foreach ($given as $givn => $count) {
5832
			if ($tot == 0) {
5833
				$per = 0;
5834
			} else {
5835
				$per = round(100 * $count / $tot_indi, 0);
5836
			}
5837
			$chd .= $this->arrayToExtendedEncoding([$per]);
5838
			$chl[] = $givn . ' - ' . I18N::number($count);
5839
		}
5840
		$per = round(100 * ($tot_indi - $tot) / $tot_indi, 0);
5841
		$chd .= $this->arrayToExtendedEncoding([$per]);
5842
		$chl[] = I18N::translate('Other') . ' - ' . I18N::number($tot_indi - $tot);
5843
5844
		$chart_title = implode(I18N::$list_separator, $chl);
5845
		$chl         = implode('|', $chl);
5846
5847
		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 . '" />';
5848
	}
5849
5850
	/**
5851
	 * Who is currently logged in?
5852
	 *
5853
	 * @param string $type
5854
	 *
5855
	 * @return string
5856
	 */
5857
	private function usersLoggedInQuery($type = 'nolist') {
5858
		$content = '';
5859
		// List active users
5860
		$NumAnonymous = 0;
5861
		$loggedusers  = [];
5862
		foreach (User::allLoggedIn() as $user) {
5863
			if (Auth::isAdmin() || $user->getPreference('visibleonline')) {
5864
				$loggedusers[] = $user;
5865
			} else {
5866
				$NumAnonymous++;
5867
			}
5868
		}
5869
		$LoginUsers = count($loggedusers);
5870
		if ($LoginUsers == 0 && $NumAnonymous == 0) {
5871
			return I18N::translate('No signed-in and no anonymous users');
5872
		}
5873
		if ($NumAnonymous > 0) {
5874
			$content .= '<b>' . I18N::plural('%s anonymous signed-in user', '%s anonymous signed-in users', $NumAnonymous, I18N::number($NumAnonymous)) . '</b>';
5875
		}
5876
		if ($LoginUsers > 0) {
5877
			if ($NumAnonymous) {
5878
				if ($type == 'list') {
5879
					$content .= '<br><br>';
5880
				} else {
5881
					$content .= ' ' . I18N::translate('and') . ' ';
5882
				}
5883
			}
5884
			$content .= '<b>' . I18N::plural('%s signed-in user', '%s signed-in users', $LoginUsers, I18N::number($LoginUsers)) . '</b>';
5885
			if ($type == 'list') {
5886
				$content .= '<ul>';
5887
			} else {
5888
				$content .= ': ';
5889
			}
5890
		}
5891
		if (Auth::check()) {
5892
			foreach ($loggedusers as $user) {
5893
				if ($type == 'list') {
5894
					$content .= '<li>' . e($user->getRealName()) . ' - ' . e($user->getUserName());
5895
				} else {
5896
					$content .= e($user->getRealName()) . ' - ' . e($user->getUserName());
5897
				}
5898
				if (Auth::id() != $user->getUserId() && $user->getPreference('contactmethod') != 'none') {
5899
					if ($type == 'list') {
5900
						$content .= '<br>';
5901
					}
5902
					$content .= FontAwesome::linkIcon('email', I18N::translate('Send a message'), ['class' => 'btn btn-link', 'href' => 'message.php?to=' . rawurlencode($user->getUserName())]);
5903
				}
5904
				if ($type == 'list') {
5905
					$content .= '</li>';
5906
				}
5907
			}
5908
		}
5909
		if ($type == 'list') {
5910
			$content .= '</ul>';
5911
		}
5912
5913
		return $content;
5914
	}
5915
5916
	/**
5917
	 * NUmber of users who are currently logged in?
5918
	 *
5919
	 * @param string $type
5920
	 *
5921
	 * @return int
5922
	 */
5923
	private function usersLoggedInTotalQuery($type = 'all') {
5924
		$anon    = 0;
5925
		$visible = 0;
5926
		foreach (User::allLoggedIn() as $user) {
5927
			if (Auth::isAdmin() || $user->getPreference('visibleonline')) {
5928
				$visible++;
5929
			} else {
5930
				$anon++;
5931
			}
5932
		}
5933
		if ($type == 'anon') {
5934
			return $anon;
5935
		} elseif ($type == 'visible') {
5936
			return $visible;
5937
		} else {
5938
			return $visible + $anon;
5939
		}
5940
	}
5941
5942
	/**
5943
	 * Who is currently logged in?
5944
	 *
5945
	 * @return string
5946
	 */
5947
	public function usersLoggedIn() {
5948
		return $this->usersLoggedInQuery('nolist');
5949
	}
5950
5951
	/**
5952
	 * Who is currently logged in?
5953
	 *
5954
	 * @return string
5955
	 */
5956
	public function usersLoggedInList() {
5957
		return $this->usersLoggedInQuery('list');
5958
	}
5959
5960
	/**
5961
	 * Who is currently logged in?
5962
	 *
5963
	 * @return int
5964
	 */
5965
	public function usersLoggedInTotal() {
5966
		return $this->usersLoggedInTotalQuery('all');
5967
	}
5968
5969
	/**
5970
	 * Which visitors are currently logged in?
5971
	 *
5972
	 * @return int
5973
	 */
5974
	public function usersLoggedInTotalAnon() {
5975
		return $this->usersLoggedInTotalQuery('anon');
5976
	}
5977
5978
	/**
5979
	 * Which visitors are currently logged in?
5980
	 *
5981
	 * @return int
5982
	 */
5983
	public function usersLoggedInTotalVisible() {
5984
		return $this->usersLoggedInTotalQuery('visible');
5985
	}
5986
5987
	/**
5988
	 * Get the current user's ID.
5989
	 *
5990
	 * @return null|string
5991
	 */
5992
	public function userId() {
5993
		return Auth::id();
5994
	}
5995
5996
	/**
5997
	 * Get the current user's username.
5998
	 *
5999
	 * @param string[] $params
6000
	 *
6001
	 * @return string
6002
	 */
6003
	public function userName($params = []) {
6004
		if (Auth::check()) {
6005
			return e(Auth::user()->getUserName());
6006
		} elseif (isset($params[0]) && $params[0] != '') {
6007
			// if #username:visitor# was specified, then "visitor" will be returned when the user is not logged in
6008
			return e($params[0]);
6009
		} else {
6010
			return '';
6011
		}
6012
	}
6013
6014
	/**
6015
	 * Get the current user's full name.
6016
	 *
6017
	 * @return string
6018
	 */
6019
	public function userFullName() {
6020
		return Auth::check() ? Auth::user()->getRealNameHtml() : '';
6021
	}
6022
6023
	/**
6024
	 * Get the newest registered user.
6025
	 *
6026
	 * @param string   $type
6027
	 * @param string[] $params
6028
	 *
6029
	 * @return string
6030
	 */
6031
	private function getLatestUserData($type = 'userid', $params = []) {
6032
		static $user_id = null;
6033
6034
		if ($user_id === null) {
0 ignored issues
show
introduced by
The condition $user_id === null can never be false.
Loading history...
6035
			$user = User::findLatestToRegister();
6036
		} else {
6037
			$user = User::find($user_id);
6038
		}
6039
6040
		switch ($type) {
6041
			default:
6042
			case 'userid':
6043
				return $user->getUserId();
6044
			case 'username':
6045
				return e($user->getUserName());
6046
			case 'fullname':
6047
				return $user->getRealNameHtml();
6048
			case 'regdate':
6049
				if (is_array($params) && isset($params[0]) && $params[0] != '') {
6050
					$datestamp = $params[0];
6051
				} else {
6052
					$datestamp = I18N::dateFormat();
6053
				}
6054
6055
				return FunctionsDate::timestampToGedcomDate((int) $user->getPreference('reg_timestamp'))->display(false, $datestamp);
6056
			case 'regtime':
6057
				if (is_array($params) && isset($params[0]) && $params[0] != '') {
6058
					$datestamp = $params[0];
6059
				} else {
6060
					$datestamp = str_replace('%', '', I18N::timeFormat());
6061
				}
6062
6063
				return date($datestamp, (int) $user->getPreference('reg_timestamp'));
6064
			case 'loggedin':
6065
				if (is_array($params) && isset($params[0]) && $params[0] != '') {
6066
					$yes = $params[0];
6067
				} else {
6068
					$yes = I18N::translate('yes');
6069
				}
6070
				if (is_array($params) && isset($params[1]) && $params[1] != '') {
6071
					$no = $params[1];
6072
				} else {
6073
					$no = I18N::translate('no');
6074
				}
6075
6076
				return Database::prepare("SELECT SQL_NO_CACHE 1 FROM `##session` WHERE user_id=? LIMIT 1")->execute([$user->getUserId()])->fetchOne() ? $yes : $no;
6077
		}
6078
	}
6079
6080
	/**
6081
	 * Get the newest registered user's ID.
6082
	 *
6083
	 * @return string
6084
	 */
6085
	public function latestUserId() {
6086
		return $this->getLatestUserData('userid');
6087
	}
6088
6089
	/**
6090
	 * Get the newest registered user's username.
6091
	 *
6092
	 * @return string
6093
	 */
6094
	public function latestUserName() {
6095
		return $this->getLatestUserData('username');
6096
	}
6097
6098
	/**
6099
	 * Get the newest registered user's real name.
6100
	 *
6101
	 * @return string
6102
	 */
6103
	public function latestUserFullName() {
6104
		return $this->getLatestUserData('fullname');
6105
	}
6106
6107
	/**
6108
	 * Get the date of the newest user registration.
6109
	 *
6110
	 * @param string[] $params
6111
	 *
6112
	 * @return string
6113
	 */
6114
	public function latestUserRegDate($params = []) {
6115
		return $this->getLatestUserData('regdate', $params);
6116
	}
6117
6118
	/**
6119
	 * Find the timestamp of the latest user to register.
6120
	 *
6121
	 * @param string[] $params
6122
	 *
6123
	 * @return string
6124
	 */
6125
	public function latestUserRegTime($params = []) {
6126
		return $this->getLatestUserData('regtime', $params);
6127
	}
6128
6129
	/**
6130
	 * Find the most recent user to log in.
6131
	 *
6132
	 * @param string[] $params
6133
	 *
6134
	 * @return string
6135
	 */
6136
	public function latestUserLoggedin($params = []) {
6137
		return $this->getLatestUserData('loggedin', $params);
6138
	}
6139
6140
	/**
6141
	 * Create a link to contact the webmaster.
6142
	 *
6143
	 * @return string
6144
	 */
6145
	public function contactWebmaster() {
6146
		$user_id = $this->tree->getPreference('WEBMASTER_USER_ID');
6147
		$user    = User::find($user_id);
0 ignored issues
show
Bug introduced by
$user_id of type string is incompatible with the type null|integer expected by parameter $user_id of Fisharebest\Webtrees\User::find(). ( Ignorable by Annotation )

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

6147
		$user    = User::find(/** @scrutinizer ignore-type */ $user_id);
Loading history...
6148
		if ($user) {
0 ignored issues
show
introduced by
The condition $user can never be true.
Loading history...
6149
			return Theme::theme()->contactLink($user);
6150
		} else {
6151
			return $user_id;
6152
		}
6153
	}
6154
6155
	/**
6156
	 * Create a link to contact the genealogy contact.
6157
	 *
6158
	 * @return string
6159
	 */
6160
	public function contactGedcom() {
6161
		$user_id = $this->tree->getPreference('CONTACT_USER_ID');
6162
		$user    = User::find($user_id);
0 ignored issues
show
Bug introduced by
$user_id of type string is incompatible with the type null|integer expected by parameter $user_id of Fisharebest\Webtrees\User::find(). ( Ignorable by Annotation )

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

6162
		$user    = User::find(/** @scrutinizer ignore-type */ $user_id);
Loading history...
6163
		if ($user) {
0 ignored issues
show
introduced by
The condition $user can never be true.
Loading history...
6164
			return Theme::theme()->contactLink($user);
6165
		} else {
6166
			return $user_id;
6167
		}
6168
	}
6169
6170
	/**
6171
	 * What is the current date on the server?
6172
	 *
6173
	 * @return string
6174
	 */
6175
	public function serverDate() {
6176
		return FunctionsDate::timestampToGedcomDate(WT_TIMESTAMP)->display();
6177
	}
6178
6179
	/**
6180
	 * What is the current time on the server (in 12 hour clock)?
6181
	 *
6182
	 * @return string
6183
	 */
6184
	public function serverTime() {
6185
		return date('g:i a');
6186
	}
6187
6188
	/**
6189
	 * What is the current time on the server (in 24 hour clock)?
6190
	 *
6191
	 * @return string
6192
	 */
6193
	public function serverTime24() {
6194
		return date('G:i');
6195
	}
6196
6197
	/**
6198
	 * What is the timezone of the server.
6199
	 *
6200
	 * @return string
6201
	 */
6202
	public function serverTimezone() {
6203
		return date('T');
6204
	}
6205
6206
	/**
6207
	 * What is the client's date.
6208
	 *
6209
	 * @return string
6210
	 */
6211
	public function browserDate() {
6212
		return FunctionsDate::timestampToGedcomDate(WT_TIMESTAMP + WT_TIMESTAMP_OFFSET)->display();
6213
	}
6214
6215
	/**
6216
	 * What is the client's timestamp.
6217
	 *
6218
	 * @return string
6219
	 */
6220
	public function browserTime() {
6221
		return date(str_replace('%', '', I18N::timeFormat()), WT_TIMESTAMP + WT_TIMESTAMP_OFFSET);
6222
	}
6223
6224
	/**
6225
	 * What is the browser's tiemzone.
6226
	 *
6227
	 * @return string
6228
	 */
6229
	public function browserTimezone() {
6230
		return date('T', WT_TIMESTAMP + WT_TIMESTAMP_OFFSET);
6231
	}
6232
6233
	/**
6234
	 * What is the current version of webtrees.
6235
	 *
6236
	 * @return string
6237
	 */
6238
	public function webtreesVersion() {
6239
		return WT_VERSION;
6240
	}
6241
6242
	/**
6243
	 * These functions provide access to hitcounter for use in the HTML block.
6244
	 *
6245
	 * @param string   $page_name
6246
	 * @param string[] $params
6247
	 *
6248
	 * @return string
6249
	 */
6250
	private function hitCountQuery($page_name, $params) {
6251
		if (is_array($params) && isset($params[0]) && $params[0] != '') {
6252
			$page_parameter = $params[0];
6253
		} else {
6254
			$page_parameter = '';
6255
		}
6256
6257
		if ($page_name === null) {
0 ignored issues
show
introduced by
The condition $page_name === null can never be true.
Loading history...
6258
			// index.php?ctype=gedcom
6259
			$page_name      = 'index.php';
6260
			$page_parameter = 'gedcom:' . ($page_parameter ? Tree::findByName($page_parameter)->getTreeId() : $this->tree->getTreeId());
6261
		} elseif ($page_name == 'index.php') {
6262
			// index.php?ctype=user
6263
			$user           = User::findByIdentifier($page_parameter);
6264
			$page_parameter = 'user:' . ($user ? $user->getUserId() : Auth::id());
0 ignored issues
show
introduced by
The condition $user can never be true.
Loading history...
6265
		} else {
0 ignored issues
show
Unused Code introduced by
This else statement is empty and can be removed.

This check looks for the else branches of if statements that have no statements or where all statements have been commented out. This may be the result of changes for debugging or the code may simply be obsolete.

These else branches can be removed.

if (rand(1, 6) > 3) {
print "Check failed";
} else {
    //print "Check succeeded";
}

could be turned into

if (rand(1, 6) > 3) {
    print "Check failed";
}

This is much more concise to read.

Loading history...
6266
			// indi/fam/sour/etc.
6267
		}
6268
6269
		return '<span class="odometer">' . I18N::digits(HitCounter::getCount($this->tree, $page_name, $page_parameter)) . '</span>';
6270
	}
6271
6272
	/**
6273
	 * How many times has a page been viewed.
6274
	 *
6275
	 * @param string[] $params
6276
	 *
6277
	 * @return string
6278
	 */
6279
	public function hitCount($params = []) {
6280
		return $this->hitCountQuery(null, $params);
6281
	}
6282
6283
	/**
6284
	 * How many times has a page been viewed.
6285
	 *
6286
	 * @param string[] $params
6287
	 *
6288
	 * @return string
6289
	 */
6290
	public function hitCountUser($params = []) {
6291
		return $this->hitCountQuery('index.php', $params);
6292
	}
6293
6294
	/**
6295
	 * How many times has a page been viewed.
6296
	 *
6297
	 * @param string[] $params
6298
	 *
6299
	 * @return string
6300
	 */
6301
	public function hitCountIndi($params = []) {
6302
		return $this->hitCountQuery('individual.php', $params);
6303
	}
6304
6305
	/**
6306
	 * How many times has a page been viewed.
6307
	 *
6308
	 * @param string[] $params
6309
	 *
6310
	 * @return string
6311
	 */
6312
	public function hitCountFam($params = []) {
6313
		return $this->hitCountQuery('family.php', $params);
6314
	}
6315
6316
	/**
6317
	 * How many times has a page been viewed.
6318
	 *
6319
	 * @param string[] $params
6320
	 *
6321
	 * @return string
6322
	 */
6323
	public function hitCountSour($params = []) {
6324
		return $this->hitCountQuery('source.php', $params);
6325
	}
6326
6327
	/**
6328
	 * How many times has a page been viewed.
6329
	 *
6330
	 * @param string[] $params
6331
	 *
6332
	 * @return string
6333
	 */
6334
	public function hitCountRepo($params = []) {
6335
		return $this->hitCountQuery('repo.php', $params);
6336
	}
6337
6338
	/**
6339
	 * How many times has a page been viewed.
6340
	 *
6341
	 * @param string[] $params
6342
	 *
6343
	 * @return string
6344
	 */
6345
	public function hitCountNote($params = []) {
6346
		return $this->hitCountQuery('note.php', $params);
6347
	}
6348
6349
	/**
6350
	 * How many times has a page been viewed.
6351
	 *
6352
	 * @param string[] $params
6353
	 *
6354
	 * @return string
6355
	 */
6356
	public function hitCountObje($params = []) {
6357
		return $this->hitCountQuery('mediaviewer.php', $params);
6358
	}
6359
6360
	/**
6361
	 * Convert numbers to Google's custom encoding.
6362
	 *
6363
	 * @link http://bendodson.com/news/google-extended-encoding-made-easy
6364
	 *
6365
	 * @param int[] $a
6366
	 *
6367
	 * @return string
6368
	 */
6369
	private function arrayToExtendedEncoding($a) {
6370
		$xencoding = WT_GOOGLE_CHART_ENCODING;
6371
6372
		$encoding = '';
6373
		foreach ($a as $value) {
6374
			if ($value < 0) {
6375
				$value = 0;
6376
			}
6377
			$first  = (int) ($value / 64);
6378
			$second = $value % 64;
6379
			$encoding .= $xencoding[(int) $first] . $xencoding[(int) $second];
6380
		}
6381
6382
		return $encoding;
6383
	}
6384
6385
	/**
6386
	 * Run an SQL query and cache the result.
6387
	 *
6388
	 * @param string $sql
6389
	 *
6390
	 * @return string[][]
6391
	 */
6392
	private function runSql($sql) {
6393
		static $cache = [];
6394
6395
		$id = md5($sql);
6396
		if (isset($cache[$id])) {
6397
			return $cache[$id];
6398
		}
6399
		$rows       = Database::prepare($sql)->fetchAll(PDO::FETCH_ASSOC);
6400
		$cache[$id] = $rows;
6401
6402
		return $rows;
6403
	}
6404
6405
	/**
6406
	 * Find the favorites for the tree.
6407
	 *
6408
	 * @return string
6409
	 */
6410
	public function gedcomFavorites() {
6411
		if (Module::getModuleByName('gedcom_favorites')) {
6412
			$block = new FamilyTreeFavoritesModule(WT_MODULES_DIR . 'gedcom_favorites');
6413
6414
			return $block->getBlock(0, false);
6415
		} else {
6416
			return '';
6417
		}
6418
	}
6419
6420
	/**
6421
	 * Find the favorites for the user.
6422
	 *
6423
	 * @return string
6424
	 */
6425
	public function userFavorites() {
6426
		if (Auth::check() && Module::getModuleByName('user_favorites')) {
6427
			$block = new UserFavoritesModule(WT_MODULES_DIR . 'gedcom_favorites');
6428
6429
			return $block->getBlock(0, false);
6430
		} else {
6431
			return '';
6432
		}
6433
	}
6434
6435
	/**
6436
	 * Find the number of favorites for the tree.
6437
	 *
6438
	 * @return int
6439
	 */
6440
	public function totalGedcomFavorites() {
6441
		if (Module::getModuleByName('gedcom_favorites')) {
6442
			return count(FamilyTreeFavoritesModule::getFavorites($this->tree));
0 ignored issues
show
Bug introduced by
The call to Fisharebest\Webtrees\Mod...sModule::getFavorites() has too few arguments starting with user_id. ( Ignorable by Annotation )

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

6442
			return count(FamilyTreeFavoritesModule::/** @scrutinizer ignore-call */ getFavorites($this->tree));

This check compares calls to functions or methods with their respective definitions. If the call has less arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
6443
		} else {
6444
			return 0;
6445
		}
6446
	}
6447
6448
	/**
6449
	 * Find the number of favorites for the user.
6450
	 *
6451
	 * @return int
6452
	 */
6453
	public function totalUserFavorites() {
6454
		if (Module::getModuleByName('user_favorites')) {
6455
			return count(UserFavoritesModule::getFavorites($this->tree, 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

6455
			return count(UserFavoritesModule::getFavorites($this->tree, /** @scrutinizer ignore-type */ Auth::id()));
Loading history...
6456
		} else {
6457
			return 0;
6458
		}
6459
	}
6460
6461
	/**
6462
	 * Create any of the other blocks.
6463
	 *
6464
	 * Use as #callBlock:block_name#
6465
	 *
6466
	 * @param string[] $params
6467
	 *
6468
	 * @return string
6469
	 */
6470
	public function callBlock($params = []) {
6471
		global $ctype;
0 ignored issues
show
Compatibility Best Practice introduced by
Use of global functionality is not recommended; it makes your code harder to test, and less reusable.

Instead of relying on global state, we recommend one of these alternatives:

1. Pass all data via parameters

function myFunction($a, $b) {
    // Do something
}

2. Create a class that maintains your state

class MyClass {
    private $a;
    private $b;

    public function __construct($a, $b) {
        $this->a = $a;
        $this->b = $b;
    }

    public function myFunction() {
        // Do something
    }
}
Loading history...
6472
6473
		if (isset($params[0]) && $params[0] != '') {
6474
			$block = $params[0];
6475
		} else {
6476
			return '';
6477
		}
6478
		$all_blocks = [];
6479
		foreach (Module::getActiveBlocks($this->tree) as $name => $active_block) {
6480
			if ($ctype == 'user' && $active_block->isUserBlock() || $ctype == 'gedcom' && $active_block->isGedcomBlock()) {
6481
				$all_blocks[$name] = $active_block;
6482
			}
6483
		}
6484
		if (!array_key_exists($block, $all_blocks) || $block == 'html') {
6485
			return '';
6486
		}
6487
		// Build the config array
6488
		array_shift($params);
6489
		$cfg = [];
6490
		foreach ($params as $config) {
6491
			$bits = explode('=', $config);
6492
			if (count($bits) < 2) {
6493
				continue;
6494
			}
6495
			$v       = array_shift($bits);
6496
			$cfg[$v] = implode('=', $bits);
6497
		}
6498
		$block    = $all_blocks[$block];
6499
		$block_id = Filter::getInteger('block_id');
6500
		$content  = $block->getBlock($block_id, false, $cfg);
6501
6502
		return $content;
6503
	}
6504
6505
	/**
6506
	 * How many messages in the user's inbox.
6507
	 *
6508
	 * @return string
6509
	 */
6510
	public function totalUserMessages() {
6511
		$total = (int) Database::prepare("SELECT SQL_CACHE COUNT(*) FROM `##message` WHERE user_id = ?")
6512
			->execute([Auth::id()])
6513
			->fetchOne();
6514
6515
		return I18N::number($total);
6516
	}
6517
6518
	/**
6519
	 * How many blog entries exist for this user.
6520
	 *
6521
	 * @return string
6522
	 */
6523
	public function totalUserJournal() {
6524
		try {
6525
			$number = (int) Database::prepare("SELECT SQL_CACHE COUNT(*) FROM `##news` WHERE user_id = ?")
6526
				->execute([Auth::id()])
6527
				->fetchOne();
6528
		} catch (PDOException $ex) {
6529
			DebugBar::addThrowable($ex);
6530
6531
			// The module may not be installed, so the table may not exist.
6532
			$number = 0;
6533
		}
6534
6535
		return I18N::number($number);
6536
	}
6537
6538
	/**
6539
	 * How many news items exist for this tree.
6540
	 *
6541
	 * @return string
6542
	 */
6543
	public function totalGedcomNews() {
6544
		try {
6545
			$number = (int) Database::prepare("SELECT SQL_CACHE COUNT(*) FROM `##news` WHERE gedcom_id = ?")
6546
				->execute([$this->tree->getTreeId()])
6547
				->fetchOne();
6548
		} catch (PDOException $ex) {
6549
			DebugBar::addThrowable($ex);
6550
6551
			// The module may not be installed, so the table may not exist.
6552
			$number = 0;
6553
		}
6554
6555
		return I18N::number($number);
6556
	}
6557
6558
	/**
6559
	 * ISO3166 3 letter codes, with their 2 letter equivalent.
6560
	 * NOTE: this is not 1:1. ENG/SCO/WAL/NIR => GB
6561
	 * NOTE: this also includes champman codes and others. Should it?
6562
	 *
6563
	 * @return string[]
6564
	 */
6565
	public function iso3166() {
6566
		return [
6567
			'ABW' => 'AW', 'AFG' => 'AF', 'AGO' => 'AO', 'AIA' => 'AI', 'ALA' => 'AX', 'ALB' => 'AL',
6568
			'AND' => 'AD', 'ARE' => 'AE', 'ARG' => 'AR', 'ARM' => 'AM', 'ASM' => 'AS',
6569
			'ATA' => 'AQ', 'ATF' => 'TF', 'ATG' => 'AG', 'AUS' => 'AU', 'AUT' => 'AT', 'AZE' => 'AZ',
6570
			'BDI' => 'BI', 'BEL' => 'BE', 'BEN' => 'BJ', 'BFA' => 'BF', 'BGD' => 'BD', 'BGR' => 'BG',
6571
			'BHR' => 'BH', 'BHS' => 'BS', 'BIH' => 'BA', 'BLR' => 'BY', 'BLZ' => 'BZ', 'BMU' => 'BM',
6572
			'BOL' => 'BO', 'BRA' => 'BR', 'BRB' => 'BB', 'BRN' => 'BN', 'BTN' => 'BT', 'BVT' => 'BV',
6573
			'BWA' => 'BW', 'CAF' => 'CF', 'CAN' => 'CA', 'CCK' => 'CC', 'CHE' => 'CH', 'CHL' => 'CL',
6574
			'CHN' => 'CN', 'CIV' => 'CI', 'CMR' => 'CM', 'COD' => 'CD', 'COG' => 'CG',
6575
			'COK' => 'CK', 'COL' => 'CO', 'COM' => 'KM', 'CPV' => 'CV', 'CRI' => 'CR', 'CUB' => 'CU',
6576
			'CXR' => 'CX', 'CYM' => 'KY', 'CYP' => 'CY', 'CZE' => 'CZ', 'DEU' => 'DE', 'DJI' => 'DJ',
6577
			'DMA' => 'DM', 'DNK' => 'DK', 'DOM' => 'DO', 'DZA' => 'DZ', 'ECU' => 'EC', 'EGY' => 'EG',
6578
			'ENG' => 'GB', 'ERI' => 'ER', 'ESH' => 'EH', 'ESP' => 'ES', 'EST' => 'EE', 'ETH' => 'ET',
6579
			'FIN' => 'FI', 'FJI' => 'FJ', 'FLK' => 'FK', 'FRA' => 'FR', 'FRO' => 'FO', 'FSM' => 'FM',
6580
			'GAB' => 'GA', 'GBR' => 'GB', 'GEO' => 'GE', 'GHA' => 'GH', 'GIB' => 'GI', 'GIN' => 'GN',
6581
			'GLP' => 'GP', 'GMB' => 'GM', 'GNB' => 'GW', 'GNQ' => 'GQ', 'GRC' => 'GR', 'GRD' => 'GD',
6582
			'GRL' => 'GL', 'GTM' => 'GT', 'GUF' => 'GF', 'GUM' => 'GU', 'GUY' => 'GY', 'HKG' => 'HK',
6583
			'HMD' => 'HM', 'HND' => 'HN', 'HRV' => 'HR', 'HTI' => 'HT', 'HUN' => 'HU', 'IDN' => 'ID',
6584
			'IND' => 'IN', 'IOT' => 'IO', 'IRL' => 'IE', 'IRN' => 'IR', 'IRQ' => 'IQ', 'ISL' => 'IS',
6585
			'ISR' => 'IL', 'ITA' => 'IT', 'JAM' => 'JM', 'JOR' => 'JO', 'JPN' => 'JA', 'KAZ' => 'KZ',
6586
			'KEN' => 'KE', 'KGZ' => 'KG', 'KHM' => 'KH', 'KIR' => 'KI', 'KNA' => 'KN', 'KOR' => 'KO',
6587
			'KWT' => 'KW', 'LAO' => 'LA', 'LBN' => 'LB', 'LBR' => 'LR', 'LBY' => 'LY', 'LCA' => 'LC',
6588
			'LIE' => 'LI', 'LKA' => 'LK', 'LSO' => 'LS', 'LTU' => 'LT', 'LUX' => 'LU', 'LVA' => 'LV',
6589
			'MAC' => 'MO', 'MAR' => 'MA', 'MCO' => 'MC', 'MDA' => 'MD', 'MDG' => 'MG', 'MDV' => 'MV',
6590
			'MEX' => 'MX', 'MHL' => 'MH', 'MKD' => 'MK', 'MLI' => 'ML', 'MLT' => 'MT', 'MMR' => 'MM',
6591
			'MNG' => 'MN', 'MNP' => 'MP', 'MNT' => 'ME', 'MOZ' => 'MZ', 'MRT' => 'MR', 'MSR' => 'MS',
6592
			'MTQ' => 'MQ', 'MUS' => 'MU', 'MWI' => 'MW', 'MYS' => 'MY', 'MYT' => 'YT', 'NAM' => 'NA',
6593
			'NCL' => 'NC', 'NER' => 'NE', 'NFK' => 'NF', 'NGA' => 'NG', 'NIC' => 'NI', 'NIR' => 'GB',
6594
			'NIU' => 'NU', 'NLD' => 'NL', 'NOR' => 'NO', 'NPL' => 'NP', 'NRU' => 'NR', 'NZL' => 'NZ',
6595
			'OMN' => 'OM', 'PAK' => 'PK', 'PAN' => 'PA', 'PCN' => 'PN', 'PER' => 'PE', 'PHL' => 'PH',
6596
			'PLW' => 'PW', 'PNG' => 'PG', 'POL' => 'PL', 'PRI' => 'PR', 'PRK' => 'KP', 'PRT' => 'PO',
6597
			'PRY' => 'PY', 'PSE' => 'PS', 'PYF' => 'PF', 'QAT' => 'QA', 'REU' => 'RE', 'ROM' => 'RO',
6598
			'RUS' => 'RU', 'RWA' => 'RW', 'SAU' => 'SA', 'SCT' => 'GB', 'SDN' => 'SD', 'SEN' => 'SN',
6599
			'SER' => 'RS', 'SGP' => 'SG', 'SGS' => 'GS', 'SHN' => 'SH', 'SJM' => 'SJ',
6600
			'SLB' => 'SB', 'SLE' => 'SL', 'SLV' => 'SV', 'SMR' => 'SM', 'SOM' => 'SO', 'SPM' => 'PM',
6601
			'STP' => 'ST', 'SUR' => 'SR', 'SVK' => 'SK', 'SVN' => 'SI', 'SWE' => 'SE',
6602
			'SWZ' => 'SZ', 'SYC' => 'SC', 'SYR' => 'SY', 'TCA' => 'TC', 'TCD' => 'TD', 'TGO' => 'TG',
6603
			'THA' => 'TH', 'TJK' => 'TJ', 'TKL' => 'TK', 'TKM' => 'TM', 'TLS' => 'TL', 'TON' => 'TO',
6604
			'TTO' => 'TT', 'TUN' => 'TN', 'TUR' => 'TR', 'TUV' => 'TV', 'TWN' => 'TW', 'TZA' => 'TZ',
6605
			'UGA' => 'UG', 'UKR' => 'UA', 'UMI' => 'UM', 'URY' => 'UY', 'USA' => 'US', 'UZB' => 'UZ',
6606
			'VAT' => 'VA', 'VCT' => 'VC', 'VEN' => 'VE', 'VGB' => 'VG', 'VIR' => 'VI', 'VNM' => 'VN',
6607
			'VUT' => 'VU', 'WLF' => 'WF', 'WLS' => 'GB', 'WSM' => 'WS', 'YEM' => 'YE', 'ZAF' => 'ZA',
6608
			'ZMB' => 'ZM', 'ZWE' => 'ZW',
6609
		];
6610
	}
6611
6612
	/**
6613
	 * Country codes and names
6614
	 *
6615
	 * @return string[]
6616
	 */
6617
	public function getAllCountries() {
6618
		return [
6619
			'???' => /* I18N: Name of a country or state */ I18N::translate('Unknown'),
6620
			'ABW' => /* I18N: Name of a country or state */ I18N::translate('Aruba'),
6621
			'AFG' => /* I18N: Name of a country or state */ I18N::translate('Afghanistan'),
6622
			'AGO' => /* I18N: Name of a country or state */ I18N::translate('Angola'),
6623
			'AIA' => /* I18N: Name of a country or state */ I18N::translate('Anguilla'),
6624
			'ALA' => /* I18N: Name of a country or state */ I18N::translate('Aland Islands'),
6625
			'ALB' => /* I18N: Name of a country or state */ I18N::translate('Albania'),
6626
			'AND' => /* I18N: Name of a country or state */ I18N::translate('Andorra'),
6627
			'ARE' => /* I18N: Name of a country or state */ I18N::translate('United Arab Emirates'),
6628
			'ARG' => /* I18N: Name of a country or state */ I18N::translate('Argentina'),
6629
			'ARM' => /* I18N: Name of a country or state */ I18N::translate('Armenia'),
6630
			'ASM' => /* I18N: Name of a country or state */ I18N::translate('American Samoa'),
6631
			'ATA' => /* I18N: Name of a country or state */ I18N::translate('Antarctica'),
6632
			'ATF' => /* I18N: Name of a country or state */ I18N::translate('French Southern Territories'),
6633
			'ATG' => /* I18N: Name of a country or state */ I18N::translate('Antigua and Barbuda'),
6634
			'AUS' => /* I18N: Name of a country or state */ I18N::translate('Australia'),
6635
			'AUT' => /* I18N: Name of a country or state */ I18N::translate('Austria'),
6636
			'AZE' => /* I18N: Name of a country or state */ I18N::translate('Azerbaijan'),
6637
			'AZR' => /* I18N: Name of a country or state */ I18N::translate('Azores'),
6638
			'BDI' => /* I18N: Name of a country or state */ I18N::translate('Burundi'),
6639
			'BEL' => /* I18N: Name of a country or state */ I18N::translate('Belgium'),
6640
			'BEN' => /* I18N: Name of a country or state */ I18N::translate('Benin'),
6641
			// BES => Bonaire, Sint Eustatius and Saba
6642
			'BFA' => /* I18N: Name of a country or state */ I18N::translate('Burkina Faso'),
6643
			'BGD' => /* I18N: Name of a country or state */ I18N::translate('Bangladesh'),
6644
			'BGR' => /* I18N: Name of a country or state */ I18N::translate('Bulgaria'),
6645
			'BHR' => /* I18N: Name of a country or state */ I18N::translate('Bahrain'),
6646
			'BHS' => /* I18N: Name of a country or state */ I18N::translate('Bahamas'),
6647
			'BIH' => /* I18N: Name of a country or state */ I18N::translate('Bosnia and Herzegovina'),
6648
			// BLM => Saint Barthélemy
6649
			'BLR' => /* I18N: Name of a country or state */ I18N::translate('Belarus'),
6650
			'BLZ' => /* I18N: Name of a country or state */ I18N::translate('Belize'),
6651
			'BMU' => /* I18N: Name of a country or state */ I18N::translate('Bermuda'),
6652
			'BOL' => /* I18N: Name of a country or state */ I18N::translate('Bolivia'),
6653
			'BRA' => /* I18N: Name of a country or state */ I18N::translate('Brazil'),
6654
			'BRB' => /* I18N: Name of a country or state */ I18N::translate('Barbados'),
6655
			'BRN' => /* I18N: Name of a country or state */ I18N::translate('Brunei Darussalam'),
6656
			'BTN' => /* I18N: Name of a country or state */ I18N::translate('Bhutan'),
6657
			'BVT' => /* I18N: Name of a country or state */ I18N::translate('Bouvet Island'),
6658
			'BWA' => /* I18N: Name of a country or state */ I18N::translate('Botswana'),
6659
			'CAF' => /* I18N: Name of a country or state */ I18N::translate('Central African Republic'),
6660
			'CAN' => /* I18N: Name of a country or state */ I18N::translate('Canada'),
6661
			'CCK' => /* I18N: Name of a country or state */ I18N::translate('Cocos (Keeling) Islands'),
6662
			'CHE' => /* I18N: Name of a country or state */ I18N::translate('Switzerland'),
6663
			'CHL' => /* I18N: Name of a country or state */ I18N::translate('Chile'),
6664
			'CHN' => /* I18N: Name of a country or state */ I18N::translate('China'),
6665
			'CIV' => /* I18N: Name of a country or state */ I18N::translate('Cote d’Ivoire'),
6666
			'CMR' => /* I18N: Name of a country or state */ I18N::translate('Cameroon'),
6667
			'COD' => /* I18N: Name of a country or state */ I18N::translate('Democratic Republic of the Congo'),
6668
			'COG' => /* I18N: Name of a country or state */ I18N::translate('Republic of the Congo'),
6669
			'COK' => /* I18N: Name of a country or state */ I18N::translate('Cook Islands'),
6670
			'COL' => /* I18N: Name of a country or state */ I18N::translate('Colombia'),
6671
			'COM' => /* I18N: Name of a country or state */ I18N::translate('Comoros'),
6672
			'CPV' => /* I18N: Name of a country or state */ I18N::translate('Cape Verde'),
6673
			'CRI' => /* I18N: Name of a country or state */ I18N::translate('Costa Rica'),
6674
			'CUB' => /* I18N: Name of a country or state */ I18N::translate('Cuba'),
6675
			// CUW => Curaçao
6676
			'CXR' => /* I18N: Name of a country or state */ I18N::translate('Christmas Island'),
6677
			'CYM' => /* I18N: Name of a country or state */ I18N::translate('Cayman Islands'),
6678
			'CYP' => /* I18N: Name of a country or state */ I18N::translate('Cyprus'),
6679
			'CZE' => /* I18N: Name of a country or state */ I18N::translate('Czech Republic'),
6680
			'DEU' => /* I18N: Name of a country or state */ I18N::translate('Germany'),
6681
			'DJI' => /* I18N: Name of a country or state */ I18N::translate('Djibouti'),
6682
			'DMA' => /* I18N: Name of a country or state */ I18N::translate('Dominica'),
6683
			'DNK' => /* I18N: Name of a country or state */ I18N::translate('Denmark'),
6684
			'DOM' => /* I18N: Name of a country or state */ I18N::translate('Dominican Republic'),
6685
			'DZA' => /* I18N: Name of a country or state */ I18N::translate('Algeria'),
6686
			'ECU' => /* I18N: Name of a country or state */ I18N::translate('Ecuador'),
6687
			'EGY' => /* I18N: Name of a country or state */ I18N::translate('Egypt'),
6688
			'ENG' => /* I18N: Name of a country or state */ I18N::translate('England'),
6689
			'ERI' => /* I18N: Name of a country or state */ I18N::translate('Eritrea'),
6690
			'ESH' => /* I18N: Name of a country or state */ I18N::translate('Western Sahara'),
6691
			'ESP' => /* I18N: Name of a country or state */ I18N::translate('Spain'),
6692
			'EST' => /* I18N: Name of a country or state */ I18N::translate('Estonia'),
6693
			'ETH' => /* I18N: Name of a country or state */ I18N::translate('Ethiopia'),
6694
			'FIN' => /* I18N: Name of a country or state */ I18N::translate('Finland'),
6695
			'FJI' => /* I18N: Name of a country or state */ I18N::translate('Fiji'),
6696
			'FLD' => /* I18N: Name of a country or state */ I18N::translate('Flanders'),
6697
			'FLK' => /* I18N: Name of a country or state */ I18N::translate('Falkland Islands'),
6698
			'FRA' => /* I18N: Name of a country or state */ I18N::translate('France'),
6699
			'FRO' => /* I18N: Name of a country or state */ I18N::translate('Faroe Islands'),
6700
			'FSM' => /* I18N: Name of a country or state */ I18N::translate('Micronesia'),
6701
			'GAB' => /* I18N: Name of a country or state */ I18N::translate('Gabon'),
6702
			'GBR' => /* I18N: Name of a country or state */ I18N::translate('United Kingdom'),
6703
			'GEO' => /* I18N: Name of a country or state */ I18N::translate('Georgia'),
6704
			'GGY' => /* I18N: Name of a country or state */ I18N::translate('Guernsey'),
6705
			'GHA' => /* I18N: Name of a country or state */ I18N::translate('Ghana'),
6706
			'GIB' => /* I18N: Name of a country or state */ I18N::translate('Gibraltar'),
6707
			'GIN' => /* I18N: Name of a country or state */ I18N::translate('Guinea'),
6708
			'GLP' => /* I18N: Name of a country or state */ I18N::translate('Guadeloupe'),
6709
			'GMB' => /* I18N: Name of a country or state */ I18N::translate('Gambia'),
6710
			'GNB' => /* I18N: Name of a country or state */ I18N::translate('Guinea-Bissau'),
6711
			'GNQ' => /* I18N: Name of a country or state */ I18N::translate('Equatorial Guinea'),
6712
			'GRC' => /* I18N: Name of a country or state */ I18N::translate('Greece'),
6713
			'GRD' => /* I18N: Name of a country or state */ I18N::translate('Grenada'),
6714
			'GRL' => /* I18N: Name of a country or state */ I18N::translate('Greenland'),
6715
			'GTM' => /* I18N: Name of a country or state */ I18N::translate('Guatemala'),
6716
			'GUF' => /* I18N: Name of a country or state */ I18N::translate('French Guiana'),
6717
			'GUM' => /* I18N: Name of a country or state */ I18N::translate('Guam'),
6718
			'GUY' => /* I18N: Name of a country or state */ I18N::translate('Guyana'),
6719
			'HKG' => /* I18N: Name of a country or state */ I18N::translate('Hong Kong'),
6720
			'HMD' => /* I18N: Name of a country or state */ I18N::translate('Heard Island and McDonald Islands'),
6721
			'HND' => /* I18N: Name of a country or state */ I18N::translate('Honduras'),
6722
			'HRV' => /* I18N: Name of a country or state */ I18N::translate('Croatia'),
6723
			'HTI' => /* I18N: Name of a country or state */ I18N::translate('Haiti'),
6724
			'HUN' => /* I18N: Name of a country or state */ I18N::translate('Hungary'),
6725
			'IDN' => /* I18N: Name of a country or state */ I18N::translate('Indonesia'),
6726
			'IND' => /* I18N: Name of a country or state */ I18N::translate('India'),
6727
			'IOM' => /* I18N: Name of a country or state */ I18N::translate('Isle of Man'),
6728
			'IOT' => /* I18N: Name of a country or state */ I18N::translate('British Indian Ocean Territory'),
6729
			'IRL' => /* I18N: Name of a country or state */ I18N::translate('Ireland'),
6730
			'IRN' => /* I18N: Name of a country or state */ I18N::translate('Iran'),
6731
			'IRQ' => /* I18N: Name of a country or state */ I18N::translate('Iraq'),
6732
			'ISL' => /* I18N: Name of a country or state */ I18N::translate('Iceland'),
6733
			'ISR' => /* I18N: Name of a country or state */ I18N::translate('Israel'),
6734
			'ITA' => /* I18N: Name of a country or state */ I18N::translate('Italy'),
6735
			'JAM' => /* I18N: Name of a country or state */ I18N::translate('Jamaica'),
6736
			//'JEY' => Jersey
6737
			'JOR' => /* I18N: Name of a country or state */ I18N::translate('Jordan'),
6738
			'JPN' => /* I18N: Name of a country or state */ I18N::translate('Japan'),
6739
			'KAZ' => /* I18N: Name of a country or state */ I18N::translate('Kazakhstan'),
6740
			'KEN' => /* I18N: Name of a country or state */ I18N::translate('Kenya'),
6741
			'KGZ' => /* I18N: Name of a country or state */ I18N::translate('Kyrgyzstan'),
6742
			'KHM' => /* I18N: Name of a country or state */ I18N::translate('Cambodia'),
6743
			'KIR' => /* I18N: Name of a country or state */ I18N::translate('Kiribati'),
6744
			'KNA' => /* I18N: Name of a country or state */ I18N::translate('Saint Kitts and Nevis'),
6745
			'KOR' => /* I18N: Name of a country or state */ I18N::translate('Korea'),
6746
			'KWT' => /* I18N: Name of a country or state */ I18N::translate('Kuwait'),
6747
			'LAO' => /* I18N: Name of a country or state */ I18N::translate('Laos'),
6748
			'LBN' => /* I18N: Name of a country or state */ I18N::translate('Lebanon'),
6749
			'LBR' => /* I18N: Name of a country or state */ I18N::translate('Liberia'),
6750
			'LBY' => /* I18N: Name of a country or state */ I18N::translate('Libya'),
6751
			'LCA' => /* I18N: Name of a country or state */ I18N::translate('Saint Lucia'),
6752
			'LIE' => /* I18N: Name of a country or state */ I18N::translate('Liechtenstein'),
6753
			'LKA' => /* I18N: Name of a country or state */ I18N::translate('Sri Lanka'),
6754
			'LSO' => /* I18N: Name of a country or state */ I18N::translate('Lesotho'),
6755
			'LTU' => /* I18N: Name of a country or state */ I18N::translate('Lithuania'),
6756
			'LUX' => /* I18N: Name of a country or state */ I18N::translate('Luxembourg'),
6757
			'LVA' => /* I18N: Name of a country or state */ I18N::translate('Latvia'),
6758
			'MAC' => /* I18N: Name of a country or state */ I18N::translate('Macau'),
6759
			// MAF => Saint Martin
6760
			'MAR' => /* I18N: Name of a country or state */ I18N::translate('Morocco'),
6761
			'MCO' => /* I18N: Name of a country or state */ I18N::translate('Monaco'),
6762
			'MDA' => /* I18N: Name of a country or state */ I18N::translate('Moldova'),
6763
			'MDG' => /* I18N: Name of a country or state */ I18N::translate('Madagascar'),
6764
			'MDV' => /* I18N: Name of a country or state */ I18N::translate('Maldives'),
6765
			'MEX' => /* I18N: Name of a country or state */ I18N::translate('Mexico'),
6766
			'MHL' => /* I18N: Name of a country or state */ I18N::translate('Marshall Islands'),
6767
			'MKD' => /* I18N: Name of a country or state */ I18N::translate('Macedonia'),
6768
			'MLI' => /* I18N: Name of a country or state */ I18N::translate('Mali'),
6769
			'MLT' => /* I18N: Name of a country or state */ I18N::translate('Malta'),
6770
			'MMR' => /* I18N: Name of a country or state */ I18N::translate('Myanmar'),
6771
			'MNG' => /* I18N: Name of a country or state */ I18N::translate('Mongolia'),
6772
			'MNP' => /* I18N: Name of a country or state */ I18N::translate('Northern Mariana Islands'),
6773
			'MNT' => /* I18N: Name of a country or state */ I18N::translate('Montenegro'),
6774
			'MOZ' => /* I18N: Name of a country or state */ I18N::translate('Mozambique'),
6775
			'MRT' => /* I18N: Name of a country or state */ I18N::translate('Mauritania'),
6776
			'MSR' => /* I18N: Name of a country or state */ I18N::translate('Montserrat'),
6777
			'MTQ' => /* I18N: Name of a country or state */ I18N::translate('Martinique'),
6778
			'MUS' => /* I18N: Name of a country or state */ I18N::translate('Mauritius'),
6779
			'MWI' => /* I18N: Name of a country or state */ I18N::translate('Malawi'),
6780
			'MYS' => /* I18N: Name of a country or state */ I18N::translate('Malaysia'),
6781
			'MYT' => /* I18N: Name of a country or state */ I18N::translate('Mayotte'),
6782
			'NAM' => /* I18N: Name of a country or state */ I18N::translate('Namibia'),
6783
			'NCL' => /* I18N: Name of a country or state */ I18N::translate('New Caledonia'),
6784
			'NER' => /* I18N: Name of a country or state */ I18N::translate('Niger'),
6785
			'NFK' => /* I18N: Name of a country or state */ I18N::translate('Norfolk Island'),
6786
			'NGA' => /* I18N: Name of a country or state */ I18N::translate('Nigeria'),
6787
			'NIC' => /* I18N: Name of a country or state */ I18N::translate('Nicaragua'),
6788
			'NIR' => /* I18N: Name of a country or state */ I18N::translate('Northern Ireland'),
6789
			'NIU' => /* I18N: Name of a country or state */ I18N::translate('Niue'),
6790
			'NLD' => /* I18N: Name of a country or state */ I18N::translate('Netherlands'),
6791
			'NOR' => /* I18N: Name of a country or state */ I18N::translate('Norway'),
6792
			'NPL' => /* I18N: Name of a country or state */ I18N::translate('Nepal'),
6793
			'NRU' => /* I18N: Name of a country or state */ I18N::translate('Nauru'),
6794
			'NZL' => /* I18N: Name of a country or state */ I18N::translate('New Zealand'),
6795
			'OMN' => /* I18N: Name of a country or state */ I18N::translate('Oman'),
6796
			'PAK' => /* I18N: Name of a country or state */ I18N::translate('Pakistan'),
6797
			'PAN' => /* I18N: Name of a country or state */ I18N::translate('Panama'),
6798
			'PCN' => /* I18N: Name of a country or state */ I18N::translate('Pitcairn'),
6799
			'PER' => /* I18N: Name of a country or state */ I18N::translate('Peru'),
6800
			'PHL' => /* I18N: Name of a country or state */ I18N::translate('Philippines'),
6801
			'PLW' => /* I18N: Name of a country or state */ I18N::translate('Palau'),
6802
			'PNG' => /* I18N: Name of a country or state */ I18N::translate('Papua New Guinea'),
6803
			'POL' => /* I18N: Name of a country or state */ I18N::translate('Poland'),
6804
			'PRI' => /* I18N: Name of a country or state */ I18N::translate('Puerto Rico'),
6805
			'PRK' => /* I18N: Name of a country or state */ I18N::translate('North Korea'),
6806
			'PRT' => /* I18N: Name of a country or state */ I18N::translate('Portugal'),
6807
			'PRY' => /* I18N: Name of a country or state */ I18N::translate('Paraguay'),
6808
			'PSE' => /* I18N: Name of a country or state */ I18N::translate('Occupied Palestinian Territory'),
6809
			'PYF' => /* I18N: Name of a country or state */ I18N::translate('French Polynesia'),
6810
			'QAT' => /* I18N: Name of a country or state */ I18N::translate('Qatar'),
6811
			'REU' => /* I18N: Name of a country or state */ I18N::translate('Reunion'),
6812
			'ROM' => /* I18N: Name of a country or state */ I18N::translate('Romania'),
6813
			'RUS' => /* I18N: Name of a country or state */ I18N::translate('Russia'),
6814
			'RWA' => /* I18N: Name of a country or state */ I18N::translate('Rwanda'),
6815
			'SAU' => /* I18N: Name of a country or state */ I18N::translate('Saudi Arabia'),
6816
			'SCT' => /* I18N: Name of a country or state */ I18N::translate('Scotland'),
6817
			'SDN' => /* I18N: Name of a country or state */ I18N::translate('Sudan'),
6818
			'SEA' => /* I18N: Name of a country or state */ I18N::translate('At sea'),
6819
			'SEN' => /* I18N: Name of a country or state */ I18N::translate('Senegal'),
6820
			'SER' => /* I18N: Name of a country or state */ I18N::translate('Serbia'),
6821
			'SGP' => /* I18N: Name of a country or state */ I18N::translate('Singapore'),
6822
			'SGS' => /* I18N: Name of a country or state */ I18N::translate('South Georgia and the South Sandwich Islands'),
6823
			'SHN' => /* I18N: Name of a country or state */ I18N::translate('Saint Helena'),
6824
			'SJM' => /* I18N: Name of a country or state */ I18N::translate('Svalbard and Jan Mayen'),
6825
			'SLB' => /* I18N: Name of a country or state */ I18N::translate('Solomon Islands'),
6826
			'SLE' => /* I18N: Name of a country or state */ I18N::translate('Sierra Leone'),
6827
			'SLV' => /* I18N: Name of a country or state */ I18N::translate('El Salvador'),
6828
			'SMR' => /* I18N: Name of a country or state */ I18N::translate('San Marino'),
6829
			'SOM' => /* I18N: Name of a country or state */ I18N::translate('Somalia'),
6830
			'SPM' => /* I18N: Name of a country or state */ I18N::translate('Saint Pierre and Miquelon'),
6831
			'SSD' => /* I18N: Name of a country or state */ I18N::translate('South Sudan'),
6832
			'STP' => /* I18N: Name of a country or state */ I18N::translate('Sao Tome and Principe'),
6833
			'SUR' => /* I18N: Name of a country or state */ I18N::translate('Suriname'),
6834
			'SVK' => /* I18N: Name of a country or state */ I18N::translate('Slovakia'),
6835
			'SVN' => /* I18N: Name of a country or state */ I18N::translate('Slovenia'),
6836
			'SWE' => /* I18N: Name of a country or state */ I18N::translate('Sweden'),
6837
			'SWZ' => /* I18N: Name of a country or state */ I18N::translate('Swaziland'),
6838
			// SXM => Sint Maarten
6839
			'SYC' => /* I18N: Name of a country or state */ I18N::translate('Seychelles'),
6840
			'SYR' => /* I18N: Name of a country or state */ I18N::translate('Syria'),
6841
			'TCA' => /* I18N: Name of a country or state */ I18N::translate('Turks and Caicos Islands'),
6842
			'TCD' => /* I18N: Name of a country or state */ I18N::translate('Chad'),
6843
			'TGO' => /* I18N: Name of a country or state */ I18N::translate('Togo'),
6844
			'THA' => /* I18N: Name of a country or state */ I18N::translate('Thailand'),
6845
			'TJK' => /* I18N: Name of a country or state */ I18N::translate('Tajikistan'),
6846
			'TKL' => /* I18N: Name of a country or state */ I18N::translate('Tokelau'),
6847
			'TKM' => /* I18N: Name of a country or state */ I18N::translate('Turkmenistan'),
6848
			'TLS' => /* I18N: Name of a country or state */ I18N::translate('Timor-Leste'),
6849
			'TON' => /* I18N: Name of a country or state */ I18N::translate('Tonga'),
6850
			'TTO' => /* I18N: Name of a country or state */ I18N::translate('Trinidad and Tobago'),
6851
			'TUN' => /* I18N: Name of a country or state */ I18N::translate('Tunisia'),
6852
			'TUR' => /* I18N: Name of a country or state */ I18N::translate('Turkey'),
6853
			'TUV' => /* I18N: Name of a country or state */ I18N::translate('Tuvalu'),
6854
			'TWN' => /* I18N: Name of a country or state */ I18N::translate('Taiwan'),
6855
			'TZA' => /* I18N: Name of a country or state */ I18N::translate('Tanzania'),
6856
			'UGA' => /* I18N: Name of a country or state */ I18N::translate('Uganda'),
6857
			'UKR' => /* I18N: Name of a country or state */ I18N::translate('Ukraine'),
6858
			'UMI' => /* I18N: Name of a country or state */ I18N::translate('US Minor Outlying Islands'),
6859
			'URY' => /* I18N: Name of a country or state */ I18N::translate('Uruguay'),
6860
			'USA' => /* I18N: Name of a country or state */ I18N::translate('United States'),
6861
			'UZB' => /* I18N: Name of a country or state */ I18N::translate('Uzbekistan'),
6862
			'VAT' => /* I18N: Name of a country or state */ I18N::translate('Vatican City'),
6863
			'VCT' => /* I18N: Name of a country or state */ I18N::translate('Saint Vincent and the Grenadines'),
6864
			'VEN' => /* I18N: Name of a country or state */ I18N::translate('Venezuela'),
6865
			'VGB' => /* I18N: Name of a country or state */ I18N::translate('British Virgin Islands'),
6866
			'VIR' => /* I18N: Name of a country or state */ I18N::translate('US Virgin Islands'),
6867
			'VNM' => /* I18N: Name of a country or state */ I18N::translate('Vietnam'),
6868
			'VUT' => /* I18N: Name of a country or state */ I18N::translate('Vanuatu'),
6869
			'WLF' => /* I18N: Name of a country or state */ I18N::translate('Wallis and Futuna'),
6870
			'WLS' => /* I18N: Name of a country or state */ I18N::translate('Wales'),
6871
			'WSM' => /* I18N: Name of a country or state */ I18N::translate('Samoa'),
6872
			'YEM' => /* I18N: Name of a country or state */ I18N::translate('Yemen'),
6873
			'ZAF' => /* I18N: Name of a country or state */ I18N::translate('South Africa'),
6874
			'ZMB' => /* I18N: Name of a country or state */ I18N::translate('Zambia'),
6875
			'ZWE' => /* I18N: Name of a country or state */ I18N::translate('Zimbabwe'),
6876
		];
6877
	}
6878
6879
	/**
6880
	 * Century name, English => 21st, Polish => XXI, etc.
6881
	 *
6882
	 * @param int $century
6883
	 *
6884
	 * @return string
6885
	 */
6886
	private function centuryName($century) {
6887
		if ($century < 0) {
6888
			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

6888
			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...
6889
				I18N::translate('%s BCE', I18N::number(-$century)));
6890
		}
6891
		// The current chart engine (Google charts) can't handle <sup></sup> markup
6892
		switch ($century) {
6893
			case 21:
6894
				return strip_tags(I18N::translateContext('CENTURY', '21st'));
6895
			case 20:
6896
				return strip_tags(I18N::translateContext('CENTURY', '20th'));
6897
			case 19:
6898
				return strip_tags(I18N::translateContext('CENTURY', '19th'));
6899
			case 18:
6900
				return strip_tags(I18N::translateContext('CENTURY', '18th'));
6901
			case 17:
6902
				return strip_tags(I18N::translateContext('CENTURY', '17th'));
6903
			case 16:
6904
				return strip_tags(I18N::translateContext('CENTURY', '16th'));
6905
			case 15:
6906
				return strip_tags(I18N::translateContext('CENTURY', '15th'));
6907
			case 14:
6908
				return strip_tags(I18N::translateContext('CENTURY', '14th'));
6909
			case 13:
6910
				return strip_tags(I18N::translateContext('CENTURY', '13th'));
6911
			case 12:
6912
				return strip_tags(I18N::translateContext('CENTURY', '12th'));
6913
			case 11:
6914
				return strip_tags(I18N::translateContext('CENTURY', '11th'));
6915
			case 10:
6916
				return strip_tags(I18N::translateContext('CENTURY', '10th'));
6917
			case  9:
0 ignored issues
show
Coding Style introduced by
As per coding-style, case should be followed by a single space.

As per the PSR-2 coding standard, there must be a space after the case keyword, instead of the test immediately following it.

switch (true) {
    case!isset($a):  //wrong
        doSomething();
        break;
    case !isset($b):  //right
        doSomething();
        break;
}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
6918
				return strip_tags(I18N::translateContext('CENTURY', '9th'));
6919
			case  8:
0 ignored issues
show
Coding Style introduced by
As per coding-style, case should be followed by a single space.

As per the PSR-2 coding standard, there must be a space after the case keyword, instead of the test immediately following it.

switch (true) {
    case!isset($a):  //wrong
        doSomething();
        break;
    case !isset($b):  //right
        doSomething();
        break;
}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
6920
				return strip_tags(I18N::translateContext('CENTURY', '8th'));
6921
			case  7:
0 ignored issues
show
Coding Style introduced by
As per coding-style, case should be followed by a single space.

As per the PSR-2 coding standard, there must be a space after the case keyword, instead of the test immediately following it.

switch (true) {
    case!isset($a):  //wrong
        doSomething();
        break;
    case !isset($b):  //right
        doSomething();
        break;
}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
6922
				return strip_tags(I18N::translateContext('CENTURY', '7th'));
6923
			case  6:
0 ignored issues
show
Coding Style introduced by
As per coding-style, case should be followed by a single space.

As per the PSR-2 coding standard, there must be a space after the case keyword, instead of the test immediately following it.

switch (true) {
    case!isset($a):  //wrong
        doSomething();
        break;
    case !isset($b):  //right
        doSomething();
        break;
}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
6924
				return strip_tags(I18N::translateContext('CENTURY', '6th'));
6925
			case  5:
0 ignored issues
show
Coding Style introduced by
As per coding-style, case should be followed by a single space.

As per the PSR-2 coding standard, there must be a space after the case keyword, instead of the test immediately following it.

switch (true) {
    case!isset($a):  //wrong
        doSomething();
        break;
    case !isset($b):  //right
        doSomething();
        break;
}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
6926
				return strip_tags(I18N::translateContext('CENTURY', '5th'));
6927
			case  4:
0 ignored issues
show
Coding Style introduced by
As per coding-style, case should be followed by a single space.

As per the PSR-2 coding standard, there must be a space after the case keyword, instead of the test immediately following it.

switch (true) {
    case!isset($a):  //wrong
        doSomething();
        break;
    case !isset($b):  //right
        doSomething();
        break;
}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
6928
				return strip_tags(I18N::translateContext('CENTURY', '4th'));
6929
			case  3:
0 ignored issues
show
Coding Style introduced by
As per coding-style, case should be followed by a single space.

As per the PSR-2 coding standard, there must be a space after the case keyword, instead of the test immediately following it.

switch (true) {
    case!isset($a):  //wrong
        doSomething();
        break;
    case !isset($b):  //right
        doSomething();
        break;
}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
6930
				return strip_tags(I18N::translateContext('CENTURY', '3rd'));
6931
			case  2:
0 ignored issues
show
Coding Style introduced by
As per coding-style, case should be followed by a single space.

As per the PSR-2 coding standard, there must be a space after the case keyword, instead of the test immediately following it.

switch (true) {
    case!isset($a):  //wrong
        doSomething();
        break;
    case !isset($b):  //right
        doSomething();
        break;
}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
6932
				return strip_tags(I18N::translateContext('CENTURY', '2nd'));
6933
			case  1:
0 ignored issues
show
Coding Style introduced by
As per coding-style, case should be followed by a single space.

As per the PSR-2 coding standard, there must be a space after the case keyword, instead of the test immediately following it.

switch (true) {
    case!isset($a):  //wrong
        doSomething();
        break;
    case !isset($b):  //right
        doSomething();
        break;
}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
6934
				return strip_tags(I18N::translateContext('CENTURY', '1st'));
6935
			default:
6936
				return ($century - 1) . '01-' . $century . '00';
6937
		}
6938
	}
6939
}
6940