Test Failed
Branch master (4a3c5b)
by Greg
12:31
created

FunctionsPrintLists   F

Complexity

Total Complexity 252

Size/Duplication

Total Lines 1761
Duplicated Lines 13.97 %

Importance

Changes 0
Metric Value
dl 246
loc 1761
rs 0.6314
c 0
b 0
f 0
wmc 252

14 Methods

Rating   Name   Duplication   Size   Complexity  
B surnameTable() 5 57 9
F familyTable() 77 437 64
C surnameTagCloud() 0 30 7
F sourceTable() 10 81 13
F eventsList() 26 100 24
C mediaTable() 7 78 8
F individualTable() 26 408 43
D eventsTable() 37 111 20
C chartByDecade() 18 54 14
C chartByAge() 18 61 14
D surnameList() 5 65 17
C repositoryTable() 10 52 9
C noteTable() 7 62 9
A sortableNames() 0 12 1

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like FunctionsPrintLists often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use FunctionsPrintLists, and based on these observations, apply Extract Interface, too.

1
<?php
2
/**
3
 * webtrees: online genealogy
4
 * Copyright (C) 2017 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\Functions;
17
18
use Fisharebest\Webtrees\Auth;
19
use Fisharebest\Webtrees\Database;
20
use Fisharebest\Webtrees\Datatables;
21
use Fisharebest\Webtrees\Date;
22
use Fisharebest\Webtrees\Fact;
23
use Fisharebest\Webtrees\Family;
24
use Fisharebest\Webtrees\Filter;
25
use Fisharebest\Webtrees\FontAwesome;
26
use Fisharebest\Webtrees\GedcomRecord;
27
use Fisharebest\Webtrees\GedcomTag;
28
use Fisharebest\Webtrees\Html;
29
use Fisharebest\Webtrees\I18N;
30
use Fisharebest\Webtrees\Individual;
31
use Fisharebest\Webtrees\Media;
32
use Fisharebest\Webtrees\Note;
33
use Fisharebest\Webtrees\Place;
34
use Fisharebest\Webtrees\Repository;
35
use Fisharebest\Webtrees\Source;
36
use Fisharebest\Webtrees\Tree;
37
use Ramsey\Uuid\Uuid;
38
39
/**
40
 * Class FunctionsPrintLists - create sortable lists using datatables.net
41
 */
42
class FunctionsPrintLists {
43
	/**
44
	 * Generate a SURN,GIVN and GIVN,SURN sortable name for an individual.
45
	 * This allows table data to sort by surname or given names.
46
	 *
47
	 * Use AAAA as a separator (instead of ","), as Javascript localeCompare()
48
	 * ignores punctuation and "ANN,ROACH" would sort after "ANNE,ROACH",
49
	 * instead of before it.
50
	 *
51
	 * @param Individual $individual
52
	 *
53
	 * @return string[]
54
	 */
55
	private static function sortableNames(Individual $individual) {
56
		$names   = $individual->getAllNames();
57
		$primary = $individual->getPrimaryName();
58
59
		list($surn, $givn) = explode(',', $names[$primary]['sort']);
60
61
		$givn = str_replace('@P.N.', 'AAAA', $givn);
62
		$surn = str_replace('@N.N.', 'AAAA', $surn);
63
64
		return [
65
			$surn . 'AAAA' . $givn,
66
			$givn . 'AAAA' . $surn,
67
		];
68
	}
69
70
	/**
71
	 * Print a table of individuals
72
	 *
73
	 * @param Individual[] $indiviudals
74
	 * @param string       $option
75
	 *
76
	 * @return string
77
	 */
78
	public static function individualTable($indiviudals, $option = '') {
79
		global $controller, $WT_TREE;
80
81
		$table_id = 'table-indi-' . Uuid::uuid4(); // lists requires a unique ID in case there are multiple lists per page
0 ignored issues
show
Bug introduced by
Are you sure Ramsey\Uuid\Uuid::uuid4() of type Ramsey\Uuid\UuidInterface can be used in concatenation? Consider adding a __toString()-method. ( Ignorable by Annotation )

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

81
		$table_id = 'table-indi-' . /** @scrutinizer ignore-type */ Uuid::uuid4(); // lists requires a unique ID in case there are multiple lists per page
Loading history...
82
83
		$controller
84
			->addInlineJavascript('
85
				$("#' . $table_id . '").dataTable( {
86
					dom: \'<"H"<"filtersH_' . $table_id . '">T<"dt-clear">pf<"dt-clear">irl>t<"F"pl<"dt-clear"><"filtersF_' . $table_id . '">>\',
87
					' . I18N::datatablesI18N() . ',
88
					autoWidth: false,
89
					processing: true,
90
					retrieve: true,
91
					columns: [
92
						/* Given names  */ { type: "text" },
93
						/* Surnames     */ { type: "text" },
94
						/* SOSA numnber */ { type: "num", visible: ' . ($option === 'sosa' ? 'true' : 'false') . ' },
95
						/* Birth date   */ { type: "num" },
96
						/* Anniversary  */ { type: "num" },
97
						/* Birthplace   */ { type: "text" },
98
						/* Children     */ { type: "num" },
99
						/* Deate date   */ { type: "num" },
100
						/* Anniversary  */ { type: "num" },
101
						/* Age          */ { type: "num" },
102
						/* Death place  */ { type: "text" },
103
						/* Last change  */ { visible: ' . ($WT_TREE->getPreference('SHOW_LAST_CHANGE') ? 'true' : 'false') . ' },
104
						/* Filter sex   */ { sortable: false },
105
						/* Filter birth */ { sortable: false },
106
						/* Filter death */ { sortable: false },
107
						/* Filter tree  */ { sortable: false }
108
					],
109
					sorting: [[' . ($option === 'sosa' ? '4, "asc"' : '1, "asc"') . ']],
110
					displayLength: 20,
111
					pagingType: "full_numbers"
112
				});
113
114
				$("#' . $table_id . '")
115
				/* Hide/show parents */
116
				.on("click", ".btn-toggle-parents", function() {
117
					$(this).toggleClass("ui-state-active");
118
					$(".parents", $(this).closest("table").DataTable().rows().nodes()).slideToggle();
119
				})
120
				/* Hide/show statistics */
121
				.on("click", ".btn-toggle-statistics", function() {
122
					$(this).toggleClass("ui-state-active");
123
					$("#indi_list_table-charts_' . $table_id . '").slideToggle();
124
				})
125
				/* Filter buttons in table header */
126
				.on("click", "button[data-filter-column]", function() {
127
					var btn = $(this);
128
					// De-activate the other buttons in this button group
129
					btn.siblings().removeClass("active");
130
					// Apply (or clear) this filter
131
					var col = $("#' . $table_id . '").DataTable().column(btn.data("filter-column"));
132
					if (btn.hasClass("active")) {
133
						col.search("").draw();
134
					} else {
135
						col.search(btn.data("filter-value")).draw();
136
					}
137
				});
138
			');
139
140
		$max_age = (int) $WT_TREE->getPreference('MAX_ALIVE_AGE');
141
142
		// Inititialise chart data
143
		$deat_by_age = [];
144
		for ($age = 0; $age <= $max_age; $age++) {
145
			$deat_by_age[$age] = '';
146
		}
147
		$birt_by_decade = [];
148
		$deat_by_decade = [];
149
		for ($year = 1550; $year < 2030; $year += 10) {
150
			$birt_by_decade[$year] = '';
151
			$deat_by_decade[$year] = '';
152
		}
153
154
		$html = '
155
			<div class="indi-list">
156
				<table id="' . $table_id . '">
157
					<thead>
158
						<tr>
159
							<th colspan="16">
160
								<div class="btn-toolbar d-flex justify-content-between mb-2" role="toolbar">
161
									<div class="btn-group" data-toggle="buttons">
162
										<button
163
											class="btn btn-secondary"
164
											data-filter-column="12"
165
											data-filter-value="M"
166
											title="' . I18N::translate('Show only males.') . '"
167
										>
168
										' . Individual::sexImage('M', 'large') . '
169
										</button>
170
										<button
171
											class="btn btn-secondary"
172
											data-filter-column="12"
173
											data-filter-value="F"
174
											title="' . I18N::translate('Show only females.') . '"
175
										>
176
											' . Individual::sexImage('F', 'large') . '
177
										</button>
178
										<button
179
											class="btn btn-secondary"
180
											data-filter-column="12"
181
											data-filter-value="U"
182
											title="' . I18N::translate('Show only individuals for whom the gender is not known.') . '"
183
										>
184
											' . Individual::sexImage('U', 'large') . '
185
										</button>
186
									</div>
187
									<div class="btn-group" data-toggle="buttons">
188
										<button
189
											class="btn btn-secondary"
190
											data-filter-column="14"
191
											data-filter-value="N"
192
											title="' . I18N::translate('Show individuals who are alive or couples where both partners are alive.') . '"
193
										>
194
											' . I18N::translate('Alive') . '
195
										</button>
196
										<button
197
											class="btn btn-secondary"
198
											data-filter-column="14"
199
											data-filter-value="Y"
200
											title="' . I18N::translate('Show individuals who are dead or couples where both partners are dead.') . '"
201
										>
202
											' . I18N::translate('Dead') . '
203
										</button>
204
										<button
205
											class="btn btn-secondary"
206
											data-filter-column="14"
207
											data-filter-value="YES"
208
											title="' . I18N::translate('Show individuals who died more than 100 years ago.') . '"
209
										>
210
											' . I18N::translate('Death') . '&gt;100
211
										</button>
212
										<button
213
											class="btn btn-secondary"
214
											data-filter-column="14"
215
											data-filter-value="Y100"
216
											title="' . I18N::translate('Show individuals who died within the last 100 years.') . '"
217
										>
218
											' . I18N::translate('Death') . '&lt;=100
219
										</button>
220
									</div>
221
									<div class="btn-group" data-toggle="buttons">
222
										<button
223
											class="btn btn-secondary"
224
											data-filter-column="13"
225
											data-filter-value="YES"
226
											title="' . I18N::translate('Show individuals born more than 100 years ago.') . '"
227
										>
228
											' . I18N::translate('Birth') . '&gt;100
229
										</button>
230
										<button
231
											class="btn btn-secondary"
232
											data-filter-column="13"
233
											data-filter-value="Y100"
234
											title="' . I18N::translate('Show individuals born within the last 100 years.') . '"
235
										>
236
											' . I18N::translate('Birth') . '&lt;=100
237
										</button>
238
									</div>
239
									<div class="btn-group" data-toggle="buttons">
240
										<button
241
											class="btn btn-secondary"
242
											data-filter-column="15"
243
											data-filter-value="R"
244
											title="' . I18N::translate('Show “roots” couples or individuals. These individuals may also be called “patriarchs”. They are individuals who have no parents recorded in the database.') . '"
245
										>
246
											' . I18N::translate('Roots') . '
247
										</button>
248
										<button
249
											class="btn btn-secondary"
250
											data-filter-column="15"
251
											data-filter-value="L"
252
											title="' . I18N::translate('Show “leaves” couples or individuals. These are individuals who are alive but have no children recorded in the database.') . '"
253
										>
254
											' . I18N::translate('Leaves') . '
255
										</button>
256
									</div>
257
								</div>
258
							</th>
259
						</tr>
260
						<tr>
261
							<th>' . I18N::translate('Given names') . '</th>
262
							<th>' . I18N::translate('Surname') . '</th>
263
							<th>' . /* I18N: Abbreviation for “Sosa-Stradonitz number”. This is an individual’s surname, so may need transliterating into non-latin alphabets. */ I18N::translate('Sosa') . '</th>
264
							<th>' . I18N::translate('Birth') . '</th>
265
							<th><i class="icon-reminder" title="' . I18N::translate('Anniversary') . '"></i></th>
266
							<th>' . I18N::translate('Place') . '</th>
267
							<th><i class="icon-children" title="' . I18N::translate('Children') . '"></i></th>
268
							<th>' . I18N::translate('Death') . '</th>
269
							<th><i class="icon-reminder" title="' . I18N::translate('Anniversary') . '"></i></th>
270
							<th>' . I18N::translate('Age') . '</th>
271
							<th>' . I18N::translate('Place') . '</th>
272
							<th>' . I18N::translate('Last change') . '</th>
273
							<th hidden></th>
274
							<th hidden></th>
275
							<th hidden></th>
276
							<th hidden></th>
277
						</tr>
278
					</thead>
279
					<tfoot>
280
						<tr>
281
							<th colspan="16">
282
								<div class="btn-toolbar">
283
									<div class="btn-group">
284
										<button class="ui-state-default btn-toggle-parents">
285
											' . I18N::translate('Show parents') . '
286
										</button>
287
										<button class="ui-state-default btn-toggle-statistics">
288
											' . I18N::translate('Show statistics charts') . '
289
										</button>
290
									</div>
291
								</div>
292
							</th>
293
						</tr>
294
					</tfoot>
295
					<tbody>';
296
297
		$hundred_years_ago = new Date(date('Y') - 100);
298
		$unique_indis      = []; // Don't double-count indis with multiple names.
299
300
		foreach ($indiviudals as $key => $individual) {
301
			if (!$individual->canShowName()) {
302
				continue;
303
			}
304
			if ($individual->isPendingDeletion()) {
305
				$class = ' class="old"';
306
			} elseif ($individual->isPendingAddtion()) {
307
				$class = ' class="new"';
308
			} else {
309
				$class = '';
310
			}
311
			$html .= '<tr' . $class . '>';
312
			// Extract Given names and Surnames for sorting
313
			list($surn_givn, $givn_surn) = self::sortableNames($individual);
314
315
			$html .= '<td colspan="2" data-sort="' . Html::escape($givn_surn) . '">';
316
			foreach ($individual->getAllNames() as $num => $name) {
317
				if ($name['type'] == 'NAME') {
318
					$title = '';
319
				} else {
320
					$title = 'title="' . strip_tags(GedcomTag::getLabel($name['type'], $individual)) . '"';
321
				}
322
				if ($num == $individual->getPrimaryName()) {
323
					$class     = ' class="name2"';
324
					$sex_image = $individual->getSexImage();
325
				} else {
326
					$class     = '';
327
					$sex_image = '';
328
				}
329
				$html .= '<a ' . $title . ' href="' . $individual->getHtmlUrl() . '"' . $class . '>' . $name['full'] . '</a>' . $sex_image . '<br>';
330
			}
331
			$html .= $individual->getPrimaryParentsNames('parents details1', 'none');
332
			$html .= '</td>';
333
334
			// Hidden column for sortable name
335
			$html .= '<td hidden data-sort="' . Html::escape($surn_givn) . '"></td>';
336
337
			// SOSA
338
			$html .= '<td class="center" data-sort="' . $key . '">';
339
			if ($option === 'sosa') {
340
				$html .= '<a href="relationship.php?pid1=' . $indiviudals[1] . '&amp;pid2=' . $individual->getXref() . '" title="' . I18N::translate('Relationships') . '" rel="nofollow">' . I18N::number($key) . '</a>';
341
			}
342
			$html .= '</td>';
343
344
			// Birth date
345
			$birth_dates = $individual->getAllBirthDates();
346
			$html .= '<td data-sort="' . $individual->getEstimatedBirthDate()->julianDay() . '">';
347
			foreach ($birth_dates as $n => $birth_date) {
348
				if ($n > 0) {
349
					$html .= '<br>';
350
				}
351
				$html .= $birth_date->display(true);
352
			}
353
			$html .= '</td>';
354
355
			// Birth anniversary
356 View Code Duplication
			if (isset($birth_dates[0]) && $birth_dates[0]->gregorianYear() >= 1550 && $birth_dates[0]->gregorianYear() < 2030 && !isset($unique_indis[$individual->getXref()])) {
357
				$birt_by_decade[(int) ($birth_dates[0]->gregorianYear() / 10) * 10] .= $individual->getSex();
358
				$anniversary = Date::getAge($birth_dates[0], null, 2);
359
			} else {
360
				$anniversary = '';
361
			}
362
			$html .= '<td class="center" data-sort="' . -$individual->getEstimatedBirthDate()->julianDay() . '">' . $anniversary . '</td>';
363
364
			// Birth place
365
			$html .= '<td>';
366 View Code Duplication
			foreach ($individual->getAllBirthPlaces() as $n => $birth_place) {
367
				if ($n > 0) {
368
					$html .= '<br>';
369
				}
370
				$html .= '<a href="' . $birth_place->getURL() . '" title="' . strip_tags($birth_place->getFullName()) . '">';
371
				$html .= $birth_place->getShortName() . '</a>';
372
			}
373
			$html .= '</td>';
374
375
			// Number of children
376
			$number_of_children = $individual->getNumberOfChildren();
377
			$html .= '<td class="center" data-sort="' . $number_of_children . '">' . I18N::number($number_of_children) . '</td>';
378
379
			// Death date
380
			$death_dates = $individual->getAllDeathDates();
381
			$html .= '<td data-sort="' . $individual->getEstimatedDeathDate()->julianDay() . '">';
382
			foreach ($death_dates as $num => $death_date) {
383
				if ($num) {
384
					$html .= '<br>';
385
				}
386
				$html .= $death_date->display(true);
387
			}
388
			$html .= '</td>';
389
390
			// Death anniversary
391 View Code Duplication
			if (isset($death_dates[0]) && $death_dates[0]->gregorianYear() >= 1550 && $death_dates[0]->gregorianYear() < 2030 && !isset($unique_indis[$individual->getXref()])) {
392
				$deat_by_decade[(int) ($death_dates[0]->gregorianYear() / 10) * 10] .= $individual->getSex();
393
				$anniversary = Date::getAge($death_dates[0], null, 2);
394
			} else {
395
				$anniversary = '';
396
			}
397
			$html .= '<td class="center" data-sort="' . -$individual->getEstimatedDeathDate()->julianDay() . '">' . $anniversary . '</td>';
398
399
			// Age at death
400
			if (isset($birth_dates[0]) && isset($death_dates[0])) {
401
				$age_at_death      = Date::getAge($birth_dates[0], $death_dates[0], 0);
402
				$age_at_death_sort = Date::getAge($birth_dates[0], $death_dates[0], 2);
403
				if (!isset($unique_indis[$individual->getXref()]) && $age_at_death >= 0 && $age_at_death <= $max_age) {
404
					$deat_by_age[$age_at_death] .= $individual->getSex();
405
				}
406
			} else {
407
				$age_at_death      = '';
408
				$age_at_death_sort = PHP_INT_MAX;
409
			}
410
			$html .= '<td class="center" data-sort="' . $age_at_death_sort . '">' . I18N::number($age_at_death) . '</td>';
0 ignored issues
show
Bug introduced by
It seems like $age_at_death 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

410
			$html .= '<td class="center" data-sort="' . $age_at_death_sort . '">' . I18N::number(/** @scrutinizer ignore-type */ $age_at_death) . '</td>';
Loading history...
411
412
			// Death place
413
			$html .= '<td>';
414 View Code Duplication
			foreach ($individual->getAllDeathPlaces() as $n => $death_place) {
415
				if ($n > 0) {
416
					$html .= '<br>';
417
				}
418
				$html .= '<a href="' . $death_place->getURL() . '" title="' . strip_tags($death_place->getFullName()) . '">';
419
				$html .= $death_place->getShortName() . '</a>';
420
			}
421
			$html .= '</td>';
422
423
			// Last change
424
			$html .= '<td data-sort="' . $individual->lastChangeTimestamp(true) . '">' . $individual->lastChangeTimestamp() . '</td>';
425
426
			// Filter by sex
427
			$html .= '<td hidden>' . $individual->getSex() . '</td>';
428
429
			// Filter by birth date
430
			$html .= '<td hidden>';
431
			if (!$individual->canShow() || Date::compare($individual->getEstimatedBirthDate(), $hundred_years_ago) > 0) {
432
				$html .= 'Y100';
433
			} else {
434
				$html .= 'YES';
435
			}
436
			$html .= '</td>';
437
438
			// Filter by death date
439
			$html .= '<td hidden>';
440
			// Died in last 100 years? Died? Not dead?
441
			if (isset($death_dates[0]) && Date::compare($death_dates[0], $hundred_years_ago) > 0) {
442
				$html .= 'Y100';
443
			} elseif ($individual->isDead()) {
444
				$html .= 'YES';
445
			} else {
446
				$html .= 'N';
447
			}
448
			$html .= '</td>';
449
450
			// Filter by roots/leaves
451
			$html .= '<td hidden>';
452
			if (!$individual->getChildFamilies()) {
453
				$html .= 'R';
454
			} elseif (!$individual->isDead() && $individual->getNumberOfChildren() < 1) {
455
				$html .= 'L';
456
				$html .= '&nbsp;';
457
			}
458
			$html .= '</td>';
459
			$html .= '</tr>';
460
461
			$unique_indis[$individual->getXref()] = true;
462
		}
463
		$html .= '
464
					</tbody>
465
				</table>
466
				<div id="indi_list_table-charts_' . $table_id . '" style="display:none">
467
					<table class="list-charts">
468
						<tr>
469
							<td>
470
								' . self::chartByDecade($birt_by_decade, I18N::translate('Decade of birth')) . '
0 ignored issues
show
Bug introduced by
It seems like $birt_by_decade can also be of type string[]; however, parameter $data of Fisharebest\Webtrees\Fun...tLists::chartByDecade() 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

470
								' . self::chartByDecade(/** @scrutinizer ignore-type */ $birt_by_decade, I18N::translate('Decade of birth')) . '
Loading history...
471
							</td>
472
							<td>
473
								' . self::chartByDecade($deat_by_decade, I18N::translate('Decade of death')) . '
474
							</td>
475
						</tr>
476
						<tr>
477
							<td colspan="2">
478
								' . self::chartByAge($deat_by_age, I18N::translate('Age related to death year')) . '
0 ignored issues
show
Bug introduced by
It seems like $deat_by_age can also be of type string[]; however, parameter $data of Fisharebest\Webtrees\Fun...rintLists::chartByAge() 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

478
								' . self::chartByAge(/** @scrutinizer ignore-type */ $deat_by_age, I18N::translate('Age related to death year')) . '
Loading history...
479
							</td>
480
						</tr>
481
					</table>
482
				</div>
483
			</div>';
484
485
		return $html;
486
	}
487
488
	/**
489
	 * Print a table of families
490
	 *
491
	 * @param Family[] $families
492
	 *
493
	 * @return string
494
	 */
495
	public static function familyTable($families) {
496
		global $WT_TREE, $controller;
497
498
		$table_id = 'table-fam-' . Uuid::uuid4(); // lists requires a unique ID in case there are multiple lists per page
0 ignored issues
show
Bug introduced by
Are you sure Ramsey\Uuid\Uuid::uuid4() of type Ramsey\Uuid\UuidInterface can be used in concatenation? Consider adding a __toString()-method. ( Ignorable by Annotation )

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

498
		$table_id = 'table-fam-' . /** @scrutinizer ignore-type */ Uuid::uuid4(); // lists requires a unique ID in case there are multiple lists per page
Loading history...
499
500
		$controller
501
			->addInlineJavascript('
502
				$("#' . $table_id . '").dataTable( {
503
					dom: \'<"H"<"filtersH_' . $table_id . '"><"dt-clear">pf<"dt-clear">irl>t<"F"pl<"dt-clear"><"filtersF_' . $table_id . '">>\',
504
					' . I18N::datatablesI18N() . ',
505
					autoWidth: false,
506
					processing: true,
507
					retrieve: true,
508
					columns: [
509
						/* Given names         */ { type: "text" },
510
						/* Surnames            */ { type: "text" },
511
						/* Age                 */ { type: "num" },
512
						/* Given names         */ { type: "text" },
513
						/* Surnames            */ { type: "text" },
514
						/* Age                 */ { type: "num" },
515
						/* Marriage date       */ { type: "num" },
516
						/* Anniversary         */ { type: "num" },
517
						/* Marriage place      */ { type: "text" },
518
						/* Children            */ { type: "num" },
519
						/* Last change         */ { visible: ' . ($WT_TREE->getPreference('SHOW_LAST_CHANGE') ? 'true' : 'false') . ' },
520
						/* Filter marriage     */ { sortable: false },
521
						/* Filter alive/dead   */ { sortable: false },
522
						/* Filter tree         */ { sortable: false }
523
					],
524
					sorting: [[1, "asc"]],
525
					displayLength: 20,
526
					pagingType: "full_numbers"
527
			});
528
529
				$("#' . $table_id . '")
530
				/* Hide/show parents */
531
				.on("click", ".btn-toggle-parents", function() {
532
					$(this).toggleClass("ui-state-active");
533
					$(".parents", $(this).closest("table").DataTable().rows().nodes()).slideToggle();
534
				})
535
				/* Hide/show statistics */
536
				.on("click",  ".btn-toggle-statistics", function() {
537
					$(this).toggleClass("ui-state-active");
538
					$("#fam_list_table-charts_' . $table_id . '").slideToggle();
539
				})
540
				/* Filter buttons in table header */
541
				.on("click", "button[data-filter-column]", function() {
542
					var btn = $(this);
543
					// De-activate the other buttons in this button group
544
					btn.siblings().removeClass("active");
545
					// Apply (or clear) this filter
546
					var col = $("#' . $table_id . '").DataTable().column(btn.data("filter-column"));
547
					if (btn.hasClass("active")) {
548
						col.search("").draw();
549
					} else {
550
						col.search(btn.data("filter-value")).draw();
551
					}
552
				});
553
		');
554
555
		$max_age = (int) $WT_TREE->getPreference('MAX_ALIVE_AGE');
556
557
		// init chart data
558
		$marr_by_age = [];
559
		for ($age = 0; $age <= $max_age; $age++) {
560
			$marr_by_age[$age] = '';
561
		}
562
		$birt_by_decade = [];
563
		$marr_by_decade = [];
564
		for ($year = 1550; $year < 2030; $year += 10) {
565
			$birt_by_decade[$year] = '';
566
			$marr_by_decade[$year] = '';
567
		}
568
569
		$html = '
570
			<div class="fam-list">
571
				<table id="' . $table_id . '">
572
					<thead>
573
						<tr>
574
							<th colspan="14">
575
								<div class="btn-toolbar d-flex justify-content-between mb-2">
576
									<div class="btn-group" data-toggle="buttons">
577
										<button
578
											class="btn btn-secondary"
579
											data-filter-column="12"
580
											data-filter-value="N"
581
											title="' . I18N::translate('Show individuals who are alive or couples where both partners are alive.') . '"
582
										>
583
											' . I18N::translate('Both alive') . '
584
										</button>
585
										<button
586
											class="btn btn-secondary"
587
											data-filter-column="12"
588
											data-filter-value="W"
589
											title="' . I18N::translate('Show couples where only the female partner is dead.') . '"
590
										>
591
											' . I18N::translate('Widower') . '
592
										</button>
593
										<button
594
											class="btn btn-secondary"
595
											data-filter-column="12"
596
											data-filter-value="H"
597
											title="' . I18N::translate('Show couples where only the male partner is dead.') . '"
598
										>
599
											' . I18N::translate('Widow') . '
600
										</button>
601
										<button
602
											class="btn btn-secondary"
603
											data-filter-column="12"
604
											data-filter-value="Y"
605
											title="' . I18N::translate('Show individuals who are dead or couples where both partners are dead.') . '"
606
										>
607
											' . I18N::translate('Both dead') . '
608
										</button>
609
									</div>
610
									<div class="btn-group" data-toggle="buttons">
611
										<button
612
											class="btn btn-secondary"
613
											data-filter-column="13"
614
											data-filter-value="R"
615
											title="' . I18N::translate('Show “roots” couples or individuals. These individuals may also be called “patriarchs”. They are individuals who have no parents recorded in the database.') . '"
616
										>
617
											' . I18N::translate('Roots') . '
618
										</button>
619
										<button
620
											class="btn btn-secondary"
621
											data-filter-column="13"
622
											data-filter-value="L"
623
											title="' . I18N::translate('Show “leaves” couples or individuals. These are individuals who are alive but have no children recorded in the database.') . '"
624
										>
625
											' . I18N::translate('Leaves') . '
626
										</button>
627
									</div>
628
									<div class="btn-group" data-toggle="buttons">
629
										<button
630
											class="btn btn-secondary"
631
											data-filter-column="11"
632
											data-filter-value="U"
633
											title="' . I18N::translate('Show couples with an unknown marriage date.') . '"
634
										>
635
											' . I18N::translate('Marriage') . '
636
										</button>
637
										<button
638
											class="btn btn-secondary"
639
											data-filter-column="11"
640
											data-filter-value="YES"
641
											title="' . I18N::translate('Show couples who married more than 100 years ago.') . '"
642
										>
643
											' . I18N::translate('Marriage') . '&gt;100
644
										</button>
645
										<button
646
											class="btn btn-secondary"
647
											data-filter-column="11"
648
											data-filter-value="Y100"
649
											title="' . I18N::translate('Show couples who married within the last 100 years.') . '"
650
										>
651
											' . I18N::translate('Marriage') . '&lt;=100
652
										</button>
653
										<button
654
											class="btn btn-secondary"
655
											data-filter-column="11"
656
											data-filter-value="D"
657
											title="' . I18N::translate('Show divorced couples.') . '"
658
										>
659
											' . I18N::translate('Divorce') . '
660
										</button>
661
										<button
662
											class="btn btn-secondary"
663
											data-filter-column="11"
664
											data-filter-value="M"
665
											title="' . I18N::translate('Show couples where either partner married more than once.') . '"
666
										>
667
											' . I18N::translate('Multiple marriages') . '
668
										</button>
669
									</div>
670
								</div>
671
							</th>
672
						</tr>
673
						<tr>
674
							<th>' . I18N::translate('Given names') . '</th>
675
							<th>' . I18N::translate('Surname') . '</th>
676
							<th>' . I18N::translate('Age') . '</th>
677
							<th>' . I18N::translate('Given names') . '</th>
678
							<th>' . I18N::translate('Surname') . '</th>
679
							<th>' . I18N::translate('Age') . '</th>
680
							<th>' . I18N::translate('Marriage') . '</th>
681
							<th><i class="icon-reminder" title="' . I18N::translate('Anniversary') . '"></i></th>
682
							<th>' . I18N::translate('Place') . '</th>
683
							<th><i class="icon-children" title="' . I18N::translate('Children') . '"></i></th>
684
							<th>' . I18N::translate('Last change') . '</th>
685
							<th hidden></th>
686
							<th hidden></th>
687
							<th hidden></th>
688
						</tr>
689
					</thead>
690
					<tfoot>
691
						<tr>
692
							<th colspan="14">
693
								<div class="btn-toolbar">
694
									<div class="btn-group">
695
										<button class="ui-state-default btn-toggle-parents">
696
											' . I18N::translate('Show parents') . '
697
										</button>
698
										<button class="ui-state-default btn-toggle-statistics">
699
											' . I18N::translate('Show statistics charts') . '
700
										</button>
701
									</div>
702
								</div>
703
							</th>
704
						</tr>
705
					</tfoot>
706
					<tbody>';
707
708
		$hundred_years_ago = new Date(date('Y') - 100);
709
710
		foreach ($families as $family) {
711
			// Retrieve husband and wife
712
			$husb = $family->getHusband();
713
			if (is_null($husb)) {
714
				$husb = new Individual('H', '0 @H@ INDI', null, $family->getTree());
715
			}
716
			$wife = $family->getWife();
717
			if (is_null($wife)) {
718
				$wife = new Individual('W', '0 @W@ INDI', null, $family->getTree());
719
			}
720
			if (!$family->canShow()) {
721
				continue;
722
			}
723
			if ($family->isPendingDeletion()) {
724
				$class = ' class="old"';
725
			} elseif ($family->isPendingAddtion()) {
726
				$class = ' class="new"';
727
			} else {
728
				$class = '';
729
			}
730
			$html .= '<tr' . $class . '>';
731
			// Husband name(s)
732
			// Extract Given names and Surnames for sorting
733
			list($surn_givn, $givn_surn) = self::sortableNames($husb);
734
735
			$html .= '<td colspan="2" data-sort="' . Html::escape($givn_surn) . '">';
736 View Code Duplication
			foreach ($husb->getAllNames() as $num => $name) {
737
				if ($name['type'] == 'NAME') {
738
					$title = '';
739
				} else {
740
					$title = 'title="' . strip_tags(GedcomTag::getLabel($name['type'], $husb)) . '"';
741
				}
742
				if ($num == $husb->getPrimaryName()) {
743
					$class     = ' class="name2"';
744
					$sex_image = $husb->getSexImage();
745
				} else {
746
					$class     = '';
747
					$sex_image = '';
748
				}
749
				// Only show married names if they are the name we are filtering by.
750
				if ($name['type'] != '_MARNM' || $num == $husb->getPrimaryName()) {
751
					$html .= '<a ' . $title . ' href="' . $family->getHtmlUrl() . '"' . $class . '>' . $name['full'] . '</a>' . $sex_image . '<br>';
752
				}
753
			}
754
			// Husband parents
755
			$html .= $husb->getPrimaryParentsNames('parents details1', 'none');
756
			$html .= '</td>';
757
758
			// Hidden column for sortable name
759
			$html .= '<td hidden data-sort="' . Html::escape($surn_givn) . '"></td>';
760
761
			// Husband age
762
			$mdate = $family->getMarriageDate();
763
			$hdate = $husb->getBirthDate();
764 View Code Duplication
			if ($hdate->isOK() && $mdate->isOK()) {
765
				if ($hdate->gregorianYear() >= 1550 && $hdate->gregorianYear() < 2030) {
766
					$birt_by_decade[(int) ($hdate->gregorianYear() / 10) * 10] .= $husb->getSex();
767
				}
768
				$hage = Date::getAge($hdate, $mdate, 0);
769
				if ($hage >= 0 && $hage <= $max_age) {
770
					$marr_by_age[$hage] .= $husb->getSex();
771
				}
772
			}
773
			$html .= '<td class="center" data=-sort="' . Date::getAge($hdate, $mdate, 1) . '">' . Date::getAge($hdate, $mdate, 2) . '</td>';
774
775
			// Wife name(s)
776
			// Extract Given names and Surnames for sorting
777
			list($surn_givn, $givn_surn) = self::sortableNames($wife);
778
			$html .= '<td colspan="2" data-sort="' . Html::escape($givn_surn) . '">';
779 View Code Duplication
			foreach ($wife->getAllNames() as $num => $name) {
780
				if ($name['type'] == 'NAME') {
781
					$title = '';
782
				} else {
783
					$title = 'title="' . strip_tags(GedcomTag::getLabel($name['type'], $wife)) . '"';
784
				}
785
				if ($num == $wife->getPrimaryName()) {
786
					$class     = ' class="name2"';
787
					$sex_image = $wife->getSexImage();
788
				} else {
789
					$class     = '';
790
					$sex_image = '';
791
				}
792
				// Only show married names if they are the name we are filtering by.
793
				if ($name['type'] != '_MARNM' || $num == $wife->getPrimaryName()) {
794
					$html .= '<a ' . $title . ' href="' . $family->getHtmlUrl() . '"' . $class . '>' . $name['full'] . '</a>' . $sex_image . '<br>';
795
				}
796
			}
797
			// Wife parents
798
			$html .= $wife->getPrimaryParentsNames('parents details1', 'none');
799
			$html .= '</td>';
800
801
			// Hidden column for sortable name
802
			$html .= '<td hidden data-sort="' . Html::escape($surn_givn) . '"></td>';
803
804
			// Wife age
805
			$mdate = $family->getMarriageDate();
806
			$wdate = $wife->getBirthDate();
807 View Code Duplication
			if ($wdate->isOK() && $mdate->isOK()) {
808
				if ($wdate->gregorianYear() >= 1550 && $wdate->gregorianYear() < 2030) {
809
					$birt_by_decade[(int) ($wdate->gregorianYear() / 10) * 10] .= $wife->getSex();
810
				}
811
				$wage = Date::getAge($wdate, $mdate, 0);
812
				if ($wage >= 0 && $wage <= $max_age) {
813
					$marr_by_age[$wage] .= $wife->getSex();
814
				}
815
			}
816
			$html .= '<td class="center" data-sort="' . Date::getAge($wdate, $mdate, 1) . '">' . Date::getAge($wdate, $mdate, 2) . '</td>';
817
818
			// Marriage date
819
			$html .= '<td data-sort="' . $family->getMarriageDate()->julianDay() . '">';
820
			if ($marriage_dates = $family->getAllMarriageDates()) {
821
				foreach ($marriage_dates as $n => $marriage_date) {
822
					if ($n) {
823
						$html .= '<br>';
824
					}
825
					$html .= '<div>' . $marriage_date->display(true) . '</div>';
826
				}
827
				if ($marriage_dates[0]->gregorianYear() >= 1550 && $marriage_dates[0]->gregorianYear() < 2030) {
828
					$marr_by_decade[(int) ($marriage_dates[0]->gregorianYear() / 10) * 10] .= $husb->getSex() . $wife->getSex();
829
				}
830
			} elseif ($family->getFacts('_NMR')) {
831
				$html .= I18N::translate('no');
832
			} elseif ($family->getFacts('MARR')) {
833
				$html .= I18N::translate('yes');
834
			} else {
835
				$html .= '&nbsp;';
836
			}
837
			$html .= '</td>';
838
839
			// Marriage anniversary
840
			$html .= '<td class="center" data-sort="' . -$family->getMarriageDate()->julianDay() . '">' . Date::getAge($family->getMarriageDate(), null, 2) . '</td>';
841
842
			// Marriage place
843
			$html .= '<td>';
844 View Code Duplication
			foreach ($family->getAllMarriagePlaces() as $n => $marriage_place) {
845
				if ($n) {
846
					$html .= '<br>';
847
				}
848
				$html .= '<a href="' . $marriage_place->getURL() . '" title="' . strip_tags($marriage_place->getFullName()) . '">';
849
				$html .= $marriage_place->getShortName() . '</a>';
850
			}
851
			$html .= '</td>';
852
853
			// Number of children
854
			$html .= '<td class="center" data-sort="' . $family->getNumberOfChildren() . '">' . I18N::number($family->getNumberOfChildren()) . '</td>';
855
856
			// Last change
857
			$html .= '<td data-sort="' . $family->lastChangeTimestamp(true) . '">' . $family->lastChangeTimestamp() . '</td>';
858
859
			// Filter by marriage date
860
			$html .= '<td hidden>';
861
			if (!$family->canShow() || !$mdate->isOK()) {
862
				$html .= 'U';
863
			} else {
864
				if (Date::compare($mdate, $hundred_years_ago) > 0) {
865
					$html .= 'Y100';
866
				} else {
867
					$html .= 'YES';
868
				}
869
			}
870
			if ($family->getFacts(WT_EVENTS_DIV)) {
871
				$html .= 'D';
872
			}
873
			if (count($husb->getSpouseFamilies()) > 1 || count($wife->getSpouseFamilies()) > 1) {
874
				$html .= 'M';
875
			}
876
			$html .= '</td>';
877
878
			// Filter by alive/dead
879
			$html .= '<td hidden>';
880
			if ($husb->isDead() && $wife->isDead()) {
881
				$html .= 'Y';
882
			}
883 View Code Duplication
			if ($husb->isDead() && !$wife->isDead()) {
884
				if ($wife->getSex() == 'F') {
885
					$html .= 'H';
886
				}
887
				if ($wife->getSex() == 'M') {
888
					$html .= 'W';
889
				} // male partners
890
			}
891 View Code Duplication
			if (!$husb->isDead() && $wife->isDead()) {
892
				if ($husb->getSex() == 'M') {
893
					$html .= 'W';
894
				}
895
				if ($husb->getSex() == 'F') {
896
					$html .= 'H';
897
				} // female partners
898
			}
899
			if (!$husb->isDead() && !$wife->isDead()) {
900
				$html .= 'N';
901
			}
902
			$html .= '</td>';
903
904
			// Filter by roots/leaves
905
			$html .= '<td hidden>';
906
			if (!$husb->getChildFamilies() && !$wife->getChildFamilies()) {
907
				$html .= 'R';
908
			} elseif (!$husb->isDead() && !$wife->isDead() && $family->getNumberOfChildren() === 0) {
909
				$html .= 'L';
910
			}
911
			$html .= '</td>
912
			</tr>';
913
		}
914
915
		$html .= '
916
					</tbody>
917
				</table>
918
				<div id="fam_list_table-charts_' . $table_id . '" style="display:none">
919
					<table class="list-charts">
920
						<tr>
921
							<td>' . self::chartByDecade($birt_by_decade, I18N::translate('Decade of birth')) . '</td>
0 ignored issues
show
Bug introduced by
It seems like $birt_by_decade can also be of type string[]; however, parameter $data of Fisharebest\Webtrees\Fun...tLists::chartByDecade() 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

921
							<td>' . self::chartByDecade(/** @scrutinizer ignore-type */ $birt_by_decade, I18N::translate('Decade of birth')) . '</td>
Loading history...
922
							<td>' . self::chartByDecade($marr_by_decade, I18N::translate('Decade of marriage')) . '</td>
923
						</tr>
924
						<tr>
925
							<td colspan="2">' . self::chartByAge($marr_by_age, I18N::translate('Age in year of marriage')) . '</td>
0 ignored issues
show
Bug introduced by
It seems like $marr_by_age can also be of type string[]; however, parameter $data of Fisharebest\Webtrees\Fun...rintLists::chartByAge() 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

925
							<td colspan="2">' . self::chartByAge(/** @scrutinizer ignore-type */ $marr_by_age, I18N::translate('Age in year of marriage')) . '</td>
Loading history...
926
						</tr>
927
					</table>
928
				</div>
929
			</div>';
930
931
		return $html;
932
	}
933
934
	/**
935
	 * Print a table of sources
936
	 *
937
	 * @param Source[] $sources
938
	 *
939
	 * @return string
940
	 */
941
	public static function sourceTable($sources) {
942
		// Count the number of linked records. These numbers include private records.
943
		// It is not good to bypass privacy, but many servers do not have the resources
944
		// to process privacy for every record in the tree
945
		$count_individuals = Database::prepare(
946
			"SELECT CONCAT(l_to, '@', l_file), COUNT(*) FROM `##individuals` JOIN `##link` ON l_from = i_id AND l_file = i_file AND l_type = 'SOUR' GROUP BY l_to, l_file"
947
		)->fetchAssoc();
948
		$count_families = Database::prepare(
949
			"SELECT CONCAT(l_to, '@', l_file), COUNT(*) FROM `##families` JOIN `##link` ON l_from = f_id AND l_file = f_file AND l_type = 'SOUR' GROUP BY l_to, l_file"
950
		)->fetchAssoc();
951
		$count_media = Database::prepare(
952
			"SELECT CONCAT(l_to, '@', l_file), COUNT(*) FROM `##media` JOIN `##link` ON l_from = m_id AND l_file = m_file AND l_type = 'SOUR' GROUP BY l_to, l_file"
953
		)->fetchAssoc();
954
		$count_notes = Database::prepare(
955
			"SELECT CONCAT(l_to, '@', l_file), COUNT(*) FROM `##other` JOIN `##link` ON l_from = o_id AND l_file = o_file AND o_type = 'NOTE' AND l_type = 'SOUR' GROUP BY l_to, l_file"
956
		)->fetchAssoc();
957
		$html = '';
958
		$html .= '<table ' . Datatables::sourceTableAttributes() . '><thead><tr>';
959
		$html .= '<th>' . I18N::translate('Title') . '</th>';
960
		$html .= '<th>' . I18N::translate('Author') . '</th>';
961
		$html .= '<th>' . I18N::translate('Individuals') . '</th>';
962
		$html .= '<th>' . I18N::translate('Families') . '</th>';
963
		$html .= '<th>' . I18N::translate('Media objects') . '</th>';
964
		$html .= '<th>' . I18N::translate('Shared notes') . '</th>';
965
		$html .= '<th>' . I18N::translate('Last change') . '</th>';
966
		$html .= '</tr></thead>';
967
		$html .= '<tbody>';
968
969
		foreach ($sources as $source) {
970
			if (!$source->canShow()) {
971
				continue;
972
			}
973
			if ($source->isPendingDeletion()) {
974
				$class = ' class="old"';
975
			} elseif ($source->isPendingAddtion()) {
976
				$class = ' class="new"';
977
			} else {
978
				$class = '';
979
			}
980
			$html .= '<tr' . $class . '>';
981
			// Source name(s)
982
			$html .= '<td data-sort="' . Html::escape($source->getSortName()) . '">';
983 View Code Duplication
			foreach ($source->getAllNames() as $n => $name) {
984
				if ($n) {
985
					$html .= '<br>';
986
				}
987
				if ($n == $source->getPrimaryName()) {
988
					$html .= '<a class="name2" href="' . $source->getHtmlUrl() . '">' . $name['full'] . '</a>';
989
				} else {
990
					$html .= '<a href="' . $source->getHtmlUrl() . '">' . $name['full'] . '</a>';
991
				}
992
			}
993
			$html .= '</td>';
994
			// Author
995
			$auth = $source->getFirstFact('AUTH');
996
			if ($auth) {
997
				$author = $auth->getValue();
998
			} else {
999
				$author = '';
1000
			}
1001
			$html .= '<td data-sort="' . Html::escape($author) . '">' . $author . '</td>';
1002
			$key = $source->getXref() . '@' . $source->getTree()->getTreeId();
1003
			// Count of linked individuals
1004
			$num = array_key_exists($key, $count_individuals) ? $count_individuals[$key] : 0;
1005
			$html .= '<td class="center" data-sort="' . $num . '">' . I18N::number($num) . '</td>';
0 ignored issues
show
Bug introduced by
It seems like $num 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

1005
			$html .= '<td class="center" data-sort="' . $num . '">' . I18N::number(/** @scrutinizer ignore-type */ $num) . '</td>';
Loading history...
1006
			// Count of linked families
1007
			$num = array_key_exists($key, $count_families) ? $count_families[$key] : 0;
1008
			$html .= '<td class="center" data-sort="' . $num . '">' . I18N::number($num) . '</td>';
1009
			// Count of linked media objects
1010
			$num = array_key_exists($key, $count_media) ? $count_media[$key] : 0;
1011
			$html .= '<td class="center" data-sort="' . $num . '">' . I18N::number($num) . '</td>';
1012
			// Count of linked notes
1013
			$num = array_key_exists($key, $count_notes) ? $count_notes[$key] : 0;
1014
			$html .= '<td class="center" data-sort="' . $num . '">' . I18N::number($num) . '</td>';
1015
			// Last change
1016
			$html .= '<td data-sort="' . $source->lastChangeTimestamp(true) . '">' . $source->lastChangeTimestamp() . '</td>';
1017
			$html .= '</tr>';
1018
		}
1019
		$html .= '</tbody></table>';
1020
1021
		return $html;
1022
	}
1023
1024
	/**
1025
	 * Print a table of shared notes
1026
	 *
1027
	 * @param Note[] $notes
1028
	 *
1029
	 * @return string
1030
	 */
1031
	public static function noteTable($notes) {
1032
		// Count the number of linked records. These numbers include private records.
1033
		// It is not good to bypass privacy, but many servers do not have the resources
1034
		// to process privacy for every record in the tree
1035
		$count_individuals = Database::prepare(
1036
			"SELECT CONCAT(l_to, '@', l_file), COUNT(*) FROM `##individuals` JOIN `##link` ON l_from = i_id AND l_file = i_file AND l_type = 'NOTE' GROUP BY l_to, l_file"
1037
		)->fetchAssoc();
1038
		$count_families = Database::prepare(
1039
			"SELECT CONCAT(l_to, '@', l_file), COUNT(*) FROM `##families` JOIN `##link` ON l_from = f_id AND l_file = f_file AND l_type = 'NOTE' GROUP BY l_to, l_file"
1040
		)->fetchAssoc();
1041
		$count_media = Database::prepare(
1042
			"SELECT CONCAT(l_to, '@', l_file), COUNT(*) FROM `##media` JOIN `##link` ON l_from = m_id AND l_file = m_file AND l_type = 'NOTE' GROUP BY l_to, l_file"
1043
		)->fetchAssoc();
1044
		$count_sources = Database::prepare(
1045
			"SELECT CONCAT(l_to, '@', l_file), COUNT(*) FROM `##sources` JOIN `##link` ON l_from = s_id AND l_file = s_file AND l_type = 'NOTE' GROUP BY l_to, l_file"
1046
		)->fetchAssoc();
1047
1048
		$html = '';
1049
		$html .= '<table ' . Datatables::noteTableAttributes() . '><thead><tr>';
1050
		$html .= '<th>' . I18N::translate('Title') . '</th>';
1051
		$html .= '<th>' . I18N::translate('Individuals') . '</th>';
1052
		$html .= '<th>' . I18N::translate('Families') . '</th>';
1053
		$html .= '<th>' . I18N::translate('Media objects') . '</th>';
1054
		$html .= '<th>' . I18N::translate('Sources') . '</th>';
1055
		$html .= '<th>' . I18N::translate('Last change') . '</th>';
1056
		$html .= '</tr></thead>';
1057
		$html .= '<tbody>';
1058
1059
		foreach ($notes as $note) {
1060
			if (!$note->canShow()) {
1061
				continue;
1062
			}
1063 View Code Duplication
			if ($note->isPendingDeletion()) {
1064
				$class = ' class="old"';
1065
			} elseif ($note->isPendingAddtion()) {
1066
				$class = ' class="new"';
1067
			} else {
1068
				$class = '';
1069
			}
1070
			$html .= '<tr' . $class . '>';
1071
			// Count of linked notes
1072
			$html .= '<td data-sort="' . Html::escape($note->getSortName()) . '"><a class="name2" href="' . $note->getHtmlUrl() . '">' . $note->getFullName() . '</a></td>';
1073
			$key = $note->getXref() . '@' . $note->getTree()->getTreeId();
1074
			// Count of linked individuals
1075
			$num = array_key_exists($key, $count_individuals) ? $count_individuals[$key] : 0;
1076
			$html .= '<td class="center" data-sort="' . $num . '">' . I18N::number($num) . '</td>';
0 ignored issues
show
Bug introduced by
It seems like $num 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

1076
			$html .= '<td class="center" data-sort="' . $num . '">' . I18N::number(/** @scrutinizer ignore-type */ $num) . '</td>';
Loading history...
1077
			// Count of linked families
1078
			$num = array_key_exists($key, $count_families) ? $count_families[$key] : 0;
1079
			$html .= '<td class="center" data-sort="' . $num . '">' . I18N::number($num) . '</td>';
1080
			// Count of linked media objects
1081
			$num = array_key_exists($key, $count_media) ? $count_media[$key] : 0;
1082
			$html .= '<td class="center" data-sort="' . $num . '">' . I18N::number($num) . '</td>';
1083
			// Count of linked sources
1084
			$num = array_key_exists($key, $count_sources) ? $count_sources[$key] : 0;
1085
			$html .= '<td class="center" data-sort="' . $num . '">' . I18N::number($num) . '</td>';
1086
			// Last change
1087
			$html .= '<td data-sort="' . $note->lastChangeTimestamp(true) . '">' . $note->lastChangeTimestamp() . '</td>';
1088
			$html .= '</tr>';
1089
		}
1090
		$html .= '</tbody></table>';
1091
1092
		return $html;
1093
	}
1094
1095
	/**
1096
	 * Print a table of repositories
1097
	 *
1098
	 * @param Repository[] $repositories
1099
	 *
1100
	 * @return string
1101
	 */
1102
	public static function repositoryTable($repositories) {
1103
		// Count the number of linked records. These numbers include private records.
1104
		// It is not good to bypass privacy, but many servers do not have the resources
1105
		// to process privacy for every record in the tree
1106
		$count_sources = Database::prepare(
1107
			"SELECT CONCAT(l_to, '@', l_file), COUNT(*) FROM `##sources` JOIN `##link` ON l_from = s_id AND l_file = s_file AND l_type = 'REPO' GROUP BY l_to, l_file"
1108
		)->fetchAssoc();
1109
1110
		$html = '';
1111
		$html .= '<table ' . Datatables::repositoryTableAttributes() . '><thead><tr>';
1112
		$html .= '<th>' . I18N::translate('Repository name') . '</th>';
1113
		$html .= '<th>' . I18N::translate('Sources') . '</th>';
1114
		$html .= '<th>' . I18N::translate('Last change') . '</th>';
1115
		$html .= '</tr></thead>';
1116
		$html .= '<tbody>';
1117
1118
		foreach ($repositories as $repository) {
1119
			if (!$repository->canShow()) {
1120
				continue;
1121
			}
1122
			if ($repository->isPendingDeletion()) {
1123
				$class = ' class="old"';
1124
			} elseif ($repository->isPendingAddtion()) {
1125
				$class = ' class="new"';
1126
			} else {
1127
				$class = '';
1128
			}
1129
			$html .= '<tr' . $class . '>';
1130
			// Repository name(s)
1131
			$html .= '<td data-sort="' . Html::escape($repository->getSortName()) . '">';
1132 View Code Duplication
			foreach ($repository->getAllNames() as $n => $name) {
1133
				if ($n) {
1134
					$html .= '<br>';
1135
				}
1136
				if ($n == $repository->getPrimaryName()) {
1137
					$html .= '<a class="name2" href="' . $repository->getHtmlUrl() . '">' . $name['full'] . '</a>';
1138
				} else {
1139
					$html .= '<a href="' . $repository->getHtmlUrl() . '">' . $name['full'] . '</a>';
1140
				}
1141
			}
1142
			$html .= '</td>';
1143
			$key = $repository->getXref() . '@' . $repository->getTree()->getTreeId();
1144
			// Count of linked sources
1145
			$num = array_key_exists($key, $count_sources) ? $count_sources[$key] : 0;
1146
			$html .= '<td class="center" data-sort="' . $num . '">' . I18N::number($num) . '</td>';
0 ignored issues
show
Bug introduced by
It seems like $num 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

1146
			$html .= '<td class="center" data-sort="' . $num . '">' . I18N::number(/** @scrutinizer ignore-type */ $num) . '</td>';
Loading history...
1147
			// Last change
1148
			$html .= '<td data-sort="' . $repository->lastChangeTimestamp(true) . '">' . $repository->lastChangeTimestamp() . '</td>';
1149
			$html .= '</tr>';
1150
		}
1151
		$html .= '</tbody></table></div>';
1152
1153
		return $html;
1154
	}
1155
1156
	/**
1157
	 * Print a table of media objects
1158
	 *
1159
	 * @param Media[] $media_objects
1160
	 *
1161
	 * @return string
1162
	 */
1163
	public static function mediaTable($media_objects) {
1164
		global $WT_TREE, $controller;
1165
1166
		$html     = '';
1167
		$table_id = 'table-obje-' . Uuid::uuid4(); // lists requires a unique ID in case there are multiple lists per page
0 ignored issues
show
Bug introduced by
Are you sure Ramsey\Uuid\Uuid::uuid4() of type Ramsey\Uuid\UuidInterface can be used in concatenation? Consider adding a __toString()-method. ( Ignorable by Annotation )

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

1167
		$table_id = 'table-obje-' . /** @scrutinizer ignore-type */ Uuid::uuid4(); // lists requires a unique ID in case there are multiple lists per page
Loading history...
1168
		$controller
1169
			->addInlineJavascript('
1170
				$("#' . $table_id . '").dataTable({
1171
					dom: \'<"H"pf<"dt-clear">irl>t<"F"pl>\',
1172
					' . I18N::datatablesI18N() . ',
1173
					autoWidth:false,
1174
					processing: true,
1175
					columns: [
1176
						/* Thumbnail   */ { sortable: false },
1177
						/* Title       */ { type: "text" },
1178
						/* Individuals */ { type: "num" },
1179
						/* Families    */ { type: "num" },
1180
						/* Sources     */ { type: "num" },
1181
						/* Last change */ { visible: ' . ($WT_TREE->getPreference('SHOW_LAST_CHANGE') ? 'true' : 'false') . ' },
1182
					],
1183
					displayLength: 20,
1184
					pagingType: "full_numbers"
1185
				});
1186
			');
1187
1188
		$html .= '<div class="media-list">';
1189
		$html .= '<table id="' . $table_id . '"><thead><tr>';
1190
		$html .= '<th>' . I18N::translate('Media') . '</th>';
1191
		$html .= '<th>' . I18N::translate('Title') . '</th>';
1192
		$html .= '<th>' . I18N::translate('Individuals') . '</th>';
1193
		$html .= '<th>' . I18N::translate('Families') . '</th>';
1194
		$html .= '<th>' . I18N::translate('Sources') . '</th>';
1195
		$html .= '<th>' . I18N::translate('Last change') . '</th>';
1196
		$html .= '</tr></thead>';
1197
		$html .= '<tbody>';
1198
1199
		foreach ($media_objects as $media_object) {
1200
			if ($media_object->canShow()) {
1201
				$name = $media_object->getFullName();
1202 View Code Duplication
				if ($media_object->isPendingDeletion()) {
1203
					$class = ' class="old"';
1204
				} elseif ($media_object->isPendingAddtion()) {
1205
					$class = ' class="new"';
1206
				} else {
1207
					$class = '';
1208
				}
1209
				$html .= '<tr' . $class . '>';
1210
				// Media object thumbnail
1211
				$html .= '<td>';
1212
				foreach ($media_object as $media_file) {
1213
					$html .= $media_file->displayImage(100, 100, 'contain', []);
1214
				}
1215
				$html .= '</td>';
1216
				// Media object name(s)
1217
				$html .= '<td data-sort="' . Html::escape($media_object->getSortName()) . '">';
1218
				$html .= '<a href="' . $media_object->getHtmlUrl() . '" class="list_item name2">' . $name . '</a>';
1219
				if (Auth::isEditor($media_object->getTree())) {
1220
					$html .= '<br><a href="' . $media_object->getHtmlUrl() . '">' . basename($media_object->getFilename()) . '</a>';
0 ignored issues
show
Bug introduced by
The method getFilename() 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

1220
					$html .= '<br><a href="' . $media_object->getHtmlUrl() . '">' . basename($media_object->/** @scrutinizer ignore-call */ getFilename()) . '</a>';

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...
1221
				}
1222
				$html .= '</td>';
1223
1224
				// Count of linked individuals
1225
				$num = count($media_object->linkedIndividuals('OBJE'));
1226
				$html .= '<td class="center" data-sort="' . $num . '">' . I18N::number($num) . '</td>';
1227
				// Count of linked families
1228
				$num = count($media_object->linkedFamilies('OBJE'));
1229
				$html .= '<td class="center" data-sort="' . $num . '">' . I18N::number($num) . '</td>';
1230
				// Count of linked sources
1231
				$num = count($media_object->linkedSources('OBJE'));
1232
				$html .= '<td class="center" data-sort="' . $num . '">' . I18N::number($num) . '</td>';
1233
				// Last change
1234
				$html .= '<td data-sort="' . $media_object->lastChangeTimestamp(true) . '">' . $media_object->lastChangeTimestamp() . '</td>';
1235
				$html .= '</tr>';
1236
			}
1237
		}
1238
		$html .= '</tbody></table></div>';
1239
1240
		return $html;
1241
	}
1242
1243
	/**
1244
	 * Print a table of surnames, for the top surnames block, the indi/fam lists, etc.
1245
	 *
1246
	 * @param string[][] $surnames array (of SURN, of array of SPFX_SURN, of array of PID)
1247
	 * @param string $script "indilist.php" (counts of individuals) or "famlist.php" (counts of spouses)
1248
	 * @param Tree $tree generate links for this tree
1249
	 *
1250
	 * @return string
1251
	 */
1252
	public static function surnameTable($surnames, $script, Tree $tree) {
1253
		$html = '';
1254
		if ($script == 'famlist.php') {
1255
			$col_heading = I18N::translate('Spouses');
1256
		} else {
1257
			$col_heading = I18N::translate('Individuals');
1258
		}
1259
1260
		$html .=
1261
			'<table ' . Datatables::surnameTableAttributes() . '>' .
1262
			'<thead>' .
1263
			'<tr>' .
1264
			'<th>' . I18N::translate('Surname') . '</th>' .
1265
			'<th>' . $col_heading . '</th>' .
1266
			'</tr>' .
1267
			'</thead>';
1268
1269
		$html .= '<tbody>';
1270
		foreach ($surnames as $surn => $surns) {
1271
			// Each surname links back to the indi/fam surname list
1272 View Code Duplication
			if ($surn) {
1273
				$url = $script . '?surname=' . rawurlencode($surn) . '&amp;ged=' . $tree->getNameUrl();
1274
			} else {
1275
				$url = $script . '?alpha=,&amp;ged=' . $tree->getNameUrl();
1276
			}
1277
			$html .= '<tr>';
1278
			// Surname
1279
			$html .= '<td data-sort="' . Html::escape($surn) . '">';
1280
			// Multiple surname variants, e.g. von Groot, van Groot, van der Groot, etc.
1281
			foreach ($surns as $spfxsurn => $indis) {
1282
				if ($spfxsurn) {
1283
					$html .= '<a href="' . $url . '" dir="auto">' . Html::escape($spfxsurn) . '</a><br>';
1284
				} else {
1285
					// No surname, but a value from "2 SURN"? A common workaround for toponyms, etc.
1286
					$html .= '<a href="' . $url . '" dir="auto">' . Html::escape($surn) . '</a><br>';
1287
				}
1288
			}
1289
			$html .= '</td>';
1290
			// Surname count
1291
			$subtotal = 0;
1292
			foreach ($surns as $indis) {
1293
				$subtotal += count($indis);
0 ignored issues
show
Bug introduced by
$indis of type string is incompatible with the type Countable|array expected by parameter $var of count(). ( Ignorable by Annotation )

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

1293
				$subtotal += count(/** @scrutinizer ignore-type */ $indis);
Loading history...
1294
			}
1295
			$html .= '<td class="text-center" data-sort="' . $subtotal . '">';
1296
			foreach ($surns as $indis) {
1297
				$html .= I18N::number(count($indis)) . '<br>';
1298
			}
1299
			if (count($surns) > 1) {
1300
				// More than one surname variant? Show a subtotal
1301
				$html .= I18N::number($subtotal);
1302
			}
1303
			$html .= '</td>';
1304
			$html .= '</tr>';
1305
		}
1306
		$html .= '</tbody></table>';
1307
1308
		return $html;
1309
	}
1310
1311
	/**
1312
	 * Print a tagcloud of surnames.
1313
	 *
1314
	 * @param string[][] $surnames array (of SURN, of array of SPFX_SURN, of array of PID)
1315
	 * @param string $script indilist or famlist
1316
	 * @param bool $totals show totals after each name
1317
	 * @param Tree $tree generate links to this tree
1318
	 *
1319
	 * @return string
1320
	 */
1321
	public static function surnameTagCloud($surnames, $script, $totals, Tree $tree) {
1322
		$minimum = PHP_INT_MAX;
1323
		$maximum = 1;
1324
		foreach ($surnames as $surn => $surns) {
1325
			foreach ($surns as $spfxsurn => $indis) {
1326
				$maximum = max($maximum, count($indis));
0 ignored issues
show
Bug introduced by
$indis of type string is incompatible with the type Countable|array expected by parameter $var of count(). ( Ignorable by Annotation )

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

1326
				$maximum = max($maximum, count(/** @scrutinizer ignore-type */ $indis));
Loading history...
1327
				$minimum = min($minimum, count($indis));
1328
			}
1329
		}
1330
1331
		$html = '';
1332
		foreach ($surnames as $surn => $surns) {
1333
			foreach ($surns as $spfxsurn => $indis) {
1334
				if ($maximum === $minimum) {
1335
					// All surnames occur the same number of times
1336
					$size = 150.0;
1337
				} else {
1338
					$size = 75.0 + 125.0 * (count($indis) - $minimum) / ($maximum - $minimum);
1339
				}
1340
				$html .= '<a style="font-size:' . $size . '%" href="' . $script . '?surname=' . rawurlencode($surn) . '&amp;ged=' . $tree->getNameUrl() . '">';
1341
				if ($totals) {
1342
					$html .= I18N::translate('%1$s (%2$s)', '<span dir="auto">' . $spfxsurn . '</span>', I18N::number(count($indis)));
1343
				} else {
1344
					$html .= $spfxsurn;
1345
				}
1346
				$html .= '</a> ';
1347
			}
1348
		}
1349
1350
		return '<div class="tag_cloud">' . $html . '</div>';
1351
	}
1352
1353
	/**
1354
	 * Print a list of surnames.
1355
	 *
1356
	 * @param string[][] $surnames array (of SURN, of array of SPFX_SURN, of array of PID)
1357
	 * @param int $style 1=bullet list, 2=semicolon-separated list, 3=tabulated list with up to 4 columns
1358
	 * @param bool $totals show totals after each name
1359
	 * @param string $script indilist or famlist
1360
	 * @param Tree $tree Link back to the individual list in this tree
1361
	 *
1362
	 * @return string
1363
	 */
1364
	public static function surnameList($surnames, $style, $totals, $script, Tree $tree) {
1365
		$html = [];
1366
		foreach ($surnames as $surn => $surns) {
1367
			// Each surname links back to the indilist
1368 View Code Duplication
			if ($surn) {
1369
				$url = $script . '?surname=' . urlencode($surn) . '&amp;ged=' . $tree->getNameUrl();
1370
			} else {
1371
				$url = $script . '?alpha=,&amp;ged=' . $tree->getNameUrl();
1372
			}
1373
			// If all the surnames are just case variants, then merge them into one
1374
			// Comment out this block if you want SMITH listed separately from Smith
1375
			$first_spfxsurn = null;
1376
			foreach ($surns as $spfxsurn => $indis) {
1377
				if ($first_spfxsurn) {
1378
					if (I18N::strtoupper($spfxsurn) == I18N::strtoupper($first_spfxsurn)) {
1379
						$surns[$first_spfxsurn] = array_merge($surns[$first_spfxsurn], $surns[$spfxsurn]);
1380
						unset($surns[$spfxsurn]);
1381
					}
1382
				} else {
1383
					$first_spfxsurn = $spfxsurn;
1384
				}
1385
			}
1386
			$subhtml = '<a href="' . $url . '" dir="auto">' . Html::escape(implode(I18N::$list_separator, array_keys($surns))) . '</a>';
1387
1388
			if ($totals) {
1389
				$subtotal = 0;
1390
				foreach ($surns as $indis) {
1391
					$subtotal += count($indis);
1392
				}
1393
				$subhtml .= '&nbsp;(' . I18N::number($subtotal) . ')';
1394
			}
1395
			$html[] = $subhtml;
1396
		}
1397
		switch ($style) {
1398
		case 1:
1399
			return '<ul><li>' . implode('</li><li>', $html) . '</li></ul>';
1400
		case 2:
1401
			return implode(I18N::$list_separator, $html);
1402
		case 3:
1403
			$i     = 0;
1404
			$count = count($html);
1405
			if ($count > 36) {
1406
				$col = 4;
1407
			} elseif ($count > 18) {
1408
				$col = 3;
1409
			} elseif ($count > 6) {
1410
				$col = 2;
1411
			} else {
1412
				$col = 1;
1413
			}
1414
			$newcol = ceil($count / $col);
1415
			$html2  = '<table class="list_table"><tr>';
1416
			$html2 .= '<td class="list_value" style="padding: 14px;">';
1417
1418
			foreach ($html as $surns) {
1419
				$html2 .= $surns . '<br>';
1420
				$i++;
1421
				if ($i == $newcol && $i < $count) {
1422
					$html2 .= '</td><td class="list_value" style="padding: 14px;">';
1423
					$newcol = $i + ceil($count / $col);
1424
				}
1425
			}
1426
			$html2 .= '</td></tr></table>';
1427
1428
			return $html2;
1429
		}
1430
	}
1431
	/**
1432
	 * Print a table of events
1433
	 *
1434
	 * @param int $startjd
1435
	 * @param int $endjd
1436
	 * @param string $events
1437
	 * @param bool $only_living
1438
	 * @param string $sort_by
1439
	 *
1440
	 * @return string
1441
	 */
1442
	public static function eventsTable($startjd, $endjd, $events = 'BIRT MARR DEAT', $only_living = false, $sort_by = 'anniv') {
1443
		global $controller, $WT_TREE;
1444
1445
		$html     = '';
1446
		$table_id = 'table-even-' . Uuid::uuid4(); // lists requires a unique ID in case there are multiple lists per page
0 ignored issues
show
Bug introduced by
Are you sure Ramsey\Uuid\Uuid::uuid4() of type Ramsey\Uuid\UuidInterface can be used in concatenation? Consider adding a __toString()-method. ( Ignorable by Annotation )

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

1446
		$table_id = 'table-even-' . /** @scrutinizer ignore-type */ Uuid::uuid4(); // lists requires a unique ID in case there are multiple lists per page
Loading history...
1447
		$controller
1448
			->addInlineJavascript('
1449
				$("#' . $table_id . '").dataTable({
1450
					dom: "t",
1451
					' . I18N::datatablesI18N() . ',
1452
					autoWidth: false,
1453
					paging: false,
1454
					lengthChange: false,
1455
					filter: false,
1456
					info: true,
1457
					sorting: [[ ' . ($sort_by == 'alpha' ? 0 : 1) . ', "asc"]],
1458
					columns: [
1459
						/* Name        */ { type: "text" },
1460
						/* Date        */ { type: "num" },
1461
						/* Anniversary */ { type: "num" },
1462
						/* Event       */ { type: "text" }
1463
					]
1464
				});
1465
			');
1466
1467
		// Did we have any output? Did we skip anything?
1468
		$filter          = 0;
1469
		$filtered_events = [];
1470
1471 View Code Duplication
		foreach (FunctionsDb::getEventsList($startjd, $endjd, $events, $WT_TREE) as $fact) {
1472
			$record = $fact->getParent();
1473
			// Only living people ?
1474
			if ($only_living) {
1475
				if ($record instanceof Individual && $record->isDead()) {
1476
					$filter++;
1477
					continue;
1478
				}
1479
				if ($record instanceof Family) {
1480
					$husb = $record->getHusband();
1481
					if (is_null($husb) || $husb->isDead()) {
1482
						$filter++;
1483
						continue;
1484
					}
1485
					$wife = $record->getWife();
1486
					if (is_null($wife) || $wife->isDead()) {
1487
						$filter++;
1488
						continue;
1489
					}
1490
				}
1491
			}
1492
1493
			$filtered_events[] = $fact;
1494
		}
1495
1496
		if (!empty($filtered_events)) {
1497
			$html .= '<table id="' . $table_id . '" class="width100">';
1498
			$html .= '<thead><tr>';
1499
			$html .= '<th>' . I18N::translate('Record') . '</th>';
1500
			$html .= '<th>' . GedcomTag::getLabel('DATE') . '</th>';
1501
			$html .= '<th><i class="icon-reminder" title="' . I18N::translate('Anniversary') . '"></i></th>';
1502
			$html .= '<th>' . GedcomTag::getLabel('EVEN') . '</th>';
1503
			$html .= '</tr></thead><tbody>';
1504
1505
			foreach ($filtered_events as $n => $fact) {
1506
				$record = $fact->getParent();
1507
				$html .= '<tr>';
1508
				$html .= '<td data-sort="' . Html::escape($record->getSortName()) . '">';
1509
				$html .= '<a href="' . $record->getHtmlUrl() . '">' . $record->getFullName() . '</a>';
1510
				if ($record instanceof Individual) {
1511
					$html .= $record->getSexImage();
1512
				}
1513
				$html .= '</td>';
1514
				$html .= '<td data-sort="' . $fact->jd . '">';
1515
				$html .= $fact->getDate()->display();
1516
				$html .= '</td>';
1517
				$html .= '<td class="center" data-sort="' . $fact->anniv . '">';
1518
				$html .= ($fact->anniv ? I18N::number($fact->anniv) : '');
1519
				$html .= '</td>';
1520
				$html .= '<td class="center">' . $fact->getLabel() . '</td>';
1521
				$html .= '</tr>';
1522
			}
1523
1524
			$html .= '</tbody></table>';
1525
		} else {
1526
			if ($endjd === WT_CLIENT_JD) {
1527
				// We're dealing with the Today’s Events block
1528
				if ($filter === 0) {
1529
					$html .= I18N::translate('No events exist for today.');
1530
				} else {
1531
					$html .= I18N::translate('No events for living individuals exist for today.');
1532
				}
1533
			} else {
1534
				// We're dealing with the Upcoming Events block
1535
				if ($filter === 0) {
1536 View Code Duplication
					if ($endjd === $startjd) {
1537
						$html .= I18N::translate('No events exist for tomorrow.');
1538
					} else {
1539
						$html .= /* I18N: translation for %s==1 is unused; it is translated separately as “tomorrow” */ I18N::plural('No events exist for the next %s day.', 'No events exist for the next %s days.', $endjd - $startjd + 1, I18N::number($endjd - $startjd + 1));
1540
					}
1541 View Code Duplication
				} else {
1542
					if ($endjd === $startjd) {
1543
						$html .= I18N::translate('No events for living individuals exist for tomorrow.');
1544
					} else {
1545
						// I18N: translation for %s==1 is unused; it is translated separately as “tomorrow”
1546
						$html .= I18N::plural('No events for living people exist for the next %s day.', 'No events for living people exist for the next %s days.', $endjd - $startjd + 1, I18N::number($endjd - $startjd + 1));
1547
					}
1548
				}
1549
			}
1550
		}
1551
1552
		return $html;
1553
	}
1554
1555
	/**
1556
	 * Print a list of events
1557
	 *
1558
	 * This performs the same function as print_events_table(), but formats the output differently.
1559
	 *
1560
	 * @param int $startjd
1561
	 * @param int $endjd
1562
	 * @param string $events
1563
	 * @param bool $only_living
1564
	 * @param string $sort_by
1565
	 *
1566
	 * @return string
1567
	 */
1568
	public static function eventsList($startjd, $endjd, $events = 'BIRT MARR DEAT', $only_living = false, $sort_by = 'anniv') {
1569
		global $WT_TREE;
1570
1571
		// Did we have any output? Did we skip anything?
1572
		$output          = 0;
1573
		$filter          = 0;
1574
		$filtered_events = [];
1575
		$html            = '';
1576 View Code Duplication
		foreach (FunctionsDb::getEventsList($startjd, $endjd, $events, $WT_TREE) as $fact) {
1577
			$record = $fact->getParent();
1578
			// only living people ?
1579
			if ($only_living) {
1580
				if ($record instanceof Individual && $record->isDead()) {
1581
					$filter++;
1582
					continue;
1583
				}
1584
				if ($record instanceof Family) {
1585
					$husb = $record->getHusband();
1586
					if (is_null($husb) || $husb->isDead()) {
1587
						$filter++;
1588
						continue;
1589
					}
1590
					$wife = $record->getWife();
1591
					if (is_null($wife) || $wife->isDead()) {
1592
						$filter++;
1593
						continue;
1594
					}
1595
				}
1596
			}
1597
1598
			$output++;
1599
1600
			$filtered_events[] = $fact;
1601
		}
1602
1603
		// Now we've filtered the list, we can sort by event, if required
1604
		switch ($sort_by) {
1605
		case 'anniv':
1606
			// Data is already sorted by anniversary date
1607
			break;
1608
		case 'alpha':
1609
			uasort($filtered_events, function (Fact $x, Fact $y) {
1610
				return GedcomRecord::compare($x->getParent(), $y->getParent());
1611
			});
1612
			break;
1613
		}
1614
1615
		foreach ($filtered_events as $fact) {
1616
			$record = $fact->getParent();
1617
			$html .= '<a href="' . $record->getHtmlUrl() . '" class="list_item name2">' . $record->getFullName() . '</a>';
1618
			if ($record instanceof Individual) {
1619
				$html .= $record->getSexImage();
1620
			}
1621
			$html .= '<br><div class="indent">';
1622
			$html .= $fact->getLabel() . ' — ' . $fact->getDate()->display(true);
1623
			if ($fact->anniv) {
1624
				$html .= ' (' . I18N::translate('%s year anniversary', I18N::number($fact->anniv)) . ')';
1625
			}
1626
			if (!$fact->getPlace()->isEmpty()) {
1627
				$html .= ' — <a href="' . $fact->getPlace()->getURL() . '">' . $fact->getPlace()->getFullName() . '</a>';
1628
			}
1629
			$html .= '</div>';
1630
		}
1631
1632
		// Print a final summary message about restricted/filtered facts
1633
		$summary = '';
1634
		if ($endjd == WT_CLIENT_JD) {
1635
			// We're dealing with the Today’s Events block
1636
			if ($output == 0) {
1637
				if ($filter == 0) {
1638
					$summary = I18N::translate('No events exist for today.');
1639
				} else {
1640
					$summary = I18N::translate('No events for living individuals exist for today.');
1641
				}
1642
			}
1643
		} else {
1644
			// We're dealing with the Upcoming Events block
1645
			if ($output == 0) {
1646
				if ($filter == 0) {
1647
					if ($endjd == $startjd) {
1648
						$summary = I18N::translate('No events exist for tomorrow.');
1649
					} else {
1650
						// I18N: translation for %s==1 is unused; it is translated separately as “tomorrow”
1651
						$summary = I18N::plural('No events exist for the next %s day.', 'No events exist for the next %s days.', $endjd - $startjd + 1, I18N::number($endjd - $startjd + 1));
1652
					}
1653
				} else {
1654
					if ($endjd == $startjd) {
1655
						$summary = I18N::translate('No events for living individuals exist for tomorrow.');
1656
					} else {
1657
						// I18N: translation for %s==1 is unused; it is translated separately as “tomorrow”
1658
						$summary = I18N::plural('No events for living people exist for the next %s day.', 'No events for living people exist for the next %s days.', $endjd - $startjd + 1, I18N::number($endjd - $startjd + 1));
1659
					}
1660
				}
1661
			}
1662
		}
1663
		if ($summary) {
1664
			$html .= '<b>' . $summary . '</b>';
1665
		}
1666
1667
		return $html;
1668
	}
1669
1670
	/**
1671
	 * Print a chart by age using Google chart API
1672
	 *
1673
	 * @param int[] $data
1674
	 * @param string $title
1675
	 *
1676
	 * @return string
1677
	 */
1678
	public static function chartByAge($data, $title) {
1679
		$count  = 0;
1680
		$agemax = 0;
1681
		$vmax   = 0;
1682
		$avg    = 0;
1683
		foreach ($data as $age => $v) {
1684
			$n      = strlen($v);
1685
			$vmax   = max($vmax, $n);
1686
			$agemax = max($agemax, $age);
1687
			$count += $n;
1688
			$avg += $age * $n;
1689
		}
1690
		if ($count < 1) {
1691
			return '';
1692
		}
1693
		$avg       = round($avg / $count);
1694
		$chart_url = 'https://chart.googleapis.com/chart?cht=bvs'; // chart type
1695
		$chart_url .= '&amp;chs=725x150'; // size
1696
		$chart_url .= '&amp;chbh=3,2,2'; // bvg : 4,1,2
1697
		$chart_url .= '&amp;chf=bg,s,FFFFFF99'; //background color
1698
		$chart_url .= '&amp;chco=0000FF,FFA0CB,FF0000'; // bar color
1699
		$chart_url .= '&amp;chdl=' . rawurlencode(I18N::translate('Males')) . '|' . rawurlencode(I18N::translate('Females')) . '|' . rawurlencode(I18N::translate('Average age') . ': ' . $avg); // legend & average age
1700
		$chart_url .= '&amp;chtt=' . rawurlencode($title); // title
1701
		$chart_url .= '&amp;chxt=x,y,r'; // axis labels specification
1702
		$chart_url .= '&amp;chm=V,FF0000,0,' . ($avg - 0.3) . ',1'; // average age line marker
1703
		$chart_url .= '&amp;chxl=0:|'; // label
1704
		for ($age = 0; $age <= $agemax; $age += 5) {
1705
			$chart_url .= $age . '|||||'; // x axis
1706
		}
1707
		$chart_url .= '|1:||' . rawurlencode(I18N::percentage($vmax / $count)); // y axis
1708
		$chart_url .= '|2:||';
1709
		$step = $vmax;
1710 View Code Duplication
		for ($d = $vmax; $d > 0; $d--) {
1711
			if ($vmax < ($d * 10 + 1) && ($vmax % $d) == 0) {
1712
				$step = $d;
1713
			}
1714
		}
1715 View Code Duplication
		if ($step == $vmax) {
1716
			for ($d = $vmax - 1; $d > 0; $d--) {
1717
				if (($vmax - 1) < ($d * 10 + 1) && (($vmax - 1) % $d) == 0) {
1718
					$step = $d;
1719
				}
1720
			}
1721
		}
1722
		for ($n = $step; $n < $vmax; $n += $step) {
1723
			$chart_url .= $n . '|';
1724
		}
1725
		$chart_url .= rawurlencode($vmax . ' / ' . $count); // r axis
1726
		$chart_url .= '&amp;chg=100,' . round(100 * $step / $vmax, 1) . ',1,5'; // grid
1727
		$chart_url .= '&amp;chd=s:'; // data : simple encoding from A=0 to 9=61
1728
		$CHART_ENCODING61 = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
1729 View Code Duplication
		for ($age = 0; $age <= $agemax; $age++) {
1730
			$chart_url .= $CHART_ENCODING61[(int) (substr_count($data[$age], 'M') * 61 / $vmax)];
1731
		}
1732
		$chart_url .= ',';
1733 View Code Duplication
		for ($age = 0; $age <= $agemax; $age++) {
1734
			$chart_url .= $CHART_ENCODING61[(int) (substr_count($data[$age], 'F') * 61 / $vmax)];
1735
		}
1736
		$html = '<img src="' . $chart_url . '" alt="' . $title . '" title="' . $title . '" class="gchart">';
1737
1738
		return $html;
1739
	}
1740
1741
	/**
1742
	 * Print a chart by decade using Google chart API
1743
	 *
1744
	 * @param int[] $data
1745
	 * @param string $title
1746
	 *
1747
	 * @return string
1748
	 */
1749
	public static function chartByDecade($data, $title) {
1750
		$count = 0;
1751
		$vmax  = 0;
1752
		foreach ($data as $v) {
1753
			$n    = strlen($v);
1754
			$vmax = max($vmax, $n);
1755
			$count += $n;
1756
		}
1757
		if ($count < 1) {
1758
			return '';
1759
		}
1760
		$chart_url = 'https://chart.googleapis.com/chart?cht=bvs'; // chart type
1761
		$chart_url .= '&amp;chs=360x150'; // size
1762
		$chart_url .= '&amp;chbh=3,3'; // bvg : 4,1,2
1763
		$chart_url .= '&amp;chf=bg,s,FFFFFF99'; //background color
1764
		$chart_url .= '&amp;chco=0000FF,FFA0CB'; // bar color
1765
		$chart_url .= '&amp;chtt=' . rawurlencode($title); // title
1766
		$chart_url .= '&amp;chxt=x,y,r'; // axis labels specification
1767
		$chart_url .= '&amp;chxl=0:|&lt;|||'; // <1570
1768
		for ($y = 1600; $y < 2030; $y += 50) {
1769
			$chart_url .= $y . '|||||'; // x axis
1770
		}
1771
		$chart_url .= '|1:||' . rawurlencode(I18N::percentage($vmax / $count)); // y axis
1772
		$chart_url .= '|2:||';
1773
		$step = $vmax;
1774 View Code Duplication
		for ($d = $vmax; $d > 0; $d--) {
1775
			if ($vmax < ($d * 10 + 1) && ($vmax % $d) == 0) {
1776
				$step = $d;
1777
			}
1778
		}
1779 View Code Duplication
		if ($step == $vmax) {
1780
			for ($d = $vmax - 1; $d > 0; $d--) {
1781
				if (($vmax - 1) < ($d * 10 + 1) && (($vmax - 1) % $d) == 0) {
1782
					$step = $d;
1783
				}
1784
			}
1785
		}
1786
		for ($n = $step; $n < $vmax; $n += $step) {
1787
			$chart_url .= $n . '|';
1788
		}
1789
		$chart_url .= rawurlencode($vmax . ' / ' . $count); // r axis
1790
		$chart_url .= '&amp;chg=100,' . round(100 * $step / $vmax, 1) . ',1,5'; // grid
1791
		$chart_url .= '&amp;chd=s:'; // data : simple encoding from A=0 to 9=61
1792
		$CHART_ENCODING61 = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
1793 View Code Duplication
		for ($y = 1570; $y < 2030; $y += 10) {
1794
			$chart_url .= $CHART_ENCODING61[(int) (substr_count($data[$y], 'M') * 61 / $vmax)];
1795
		}
1796
		$chart_url .= ',';
1797 View Code Duplication
		for ($y = 1570; $y < 2030; $y += 10) {
1798
			$chart_url .= $CHART_ENCODING61[(int) (substr_count($data[$y], 'F') * 61 / $vmax)];
1799
		}
1800
		$html = '<img src="' . $chart_url . '" alt="' . $title . '" title="' . $title . '" class="gchart">';
1801
1802
		return $html;
1803
	}
1804
}
1805