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

FunctionsPrintLists::repositoryTable()   C

Complexity

Conditions 9
Paths 32

Size

Total Lines 52
Code Lines 36

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 9
eloc 36
nc 32
nop 1
dl 0
loc 52
rs 6.5703
c 0
b 0
f 0

How to fix   Long Method   

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\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;
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...
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->isPendingAddition()) {
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="' . e($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="' . e($individual->url()) . '"' . $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="' . e($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
			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
			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
			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
			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;
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...
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->isPendingAddition()) {
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="' . e($givn_surn) . '">';
736
			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="' . e($family->url()) . '"' . $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="' . e($surn_givn) . '"></td>';
760
761
			// Husband age
762
			$mdate = $family->getMarriageDate();
763
			$hdate = $husb->getBirthDate();
764
			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="' . e($givn_surn) . '">';
779
			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="' . e($family->url()) . '"' . $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="' . e($surn_givn) . '"></td>';
803
804
			// Wife age
805
			$mdate = $family->getMarriageDate();
806
			$wdate = $wife->getBirthDate();
807
			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
			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
			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
			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->isPendingAddition()) {
976
				$class = ' class="new"';
977
			} else {
978
				$class = '';
979
			}
980
			$html .= '<tr' . $class . '>';
981
			// Source name(s)
982
			$html .= '<td data-sort="' . e($source->getSortName()) . '">';
983
			foreach ($source->getAllNames() as $n => $name) {
984
				if ($n) {
985
					$html .= '<br>';
986
				}
987
				if ($n == $source->getPrimaryName()) {
988
					$html .= '<a class="name2" href="' . e($source->url()) . '">' . $name['full'] . '</a>';
989
				} else {
990
					$html .= '<a href="' . e($source->url()) . '">' . $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="' . e($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
			if ($note->isPendingDeletion()) {
1064
				$class = ' class="old"';
1065
			} elseif ($note->isPendingAddition()) {
1066
				$class = ' class="new"';
1067
			} else {
1068
				$class = '';
1069
			}
1070
			$html .= '<tr' . $class . '>';
1071
			// Count of linked notes
1072
			$html .= '<td data-sort="' . e($note->getSortName()) . '"><a class="name2" href="' . e($note->url()) . '">' . $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->isPendingAddition()) {
1125
				$class = ' class="new"';
1126
			} else {
1127
				$class = '';
1128
			}
1129
			$html .= '<tr' . $class . '>';
1130
			// Repository name(s)
1131
			$html .= '<td data-sort="' . e($repository->getSortName()) . '">';
1132
			foreach ($repository->getAllNames() as $n => $name) {
1133
				if ($n) {
1134
					$html .= '<br>';
1135
				}
1136
				if ($n == $repository->getPrimaryName()) {
1137
					$html .= '<a class="name2" href="' . e($repository->url()) . '">' . $name['full'] . '</a>';
1138
				} else {
1139
					$html .= '<a href="' . e($repository->url()) . '">' . $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;
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...
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
				if ($media_object->isPendingDeletion()) {
1203
					$class = ' class="old"';
1204
				} elseif ($media_object->isPendingAddition()) {
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="' . e($media_object->getSortName()) . '">';
1218
				$html .= '<a href="' . e($media_object->url()) . '" class="list_item name2">' . $name . '</a>';
1219
				$html .= '</td>';
1220
1221
				// Count of linked individuals
1222
				$num = count($media_object->linkedIndividuals('OBJE'));
1223
				$html .= '<td class="center" data-sort="' . $num . '">' . I18N::number($num) . '</td>';
1224
				// Count of linked families
1225
				$num = count($media_object->linkedFamilies('OBJE'));
1226
				$html .= '<td class="center" data-sort="' . $num . '">' . I18N::number($num) . '</td>';
1227
				// Count of linked sources
1228
				$num = count($media_object->linkedSources('OBJE'));
1229
				$html .= '<td class="center" data-sort="' . $num . '">' . I18N::number($num) . '</td>';
1230
				// Last change
1231
				$html .= '<td data-sort="' . $media_object->lastChangeTimestamp(true) . '">' . $media_object->lastChangeTimestamp() . '</td>';
1232
				$html .= '</tr>';
1233
			}
1234
		}
1235
		$html .= '</tbody></table></div>';
1236
1237
		return $html;
1238
	}
1239
1240
	/**
1241
	 * Print a table of surnames, for the top surnames block, the indi/fam lists, etc.
1242
	 *
1243
	 * @param string[][] $surnames array (of SURN, of array of SPFX_SURN, of array of PID)
1244
	 * @param string $script "indilist.php" (counts of individuals) or "famlist.php" (counts of spouses)
1245
	 * @param Tree $tree generate links for this tree
1246
	 *
1247
	 * @return string
1248
	 */
1249
	public static function surnameTable($surnames, $script, Tree $tree) {
1250
		$html = '';
1251
		if ($script == 'famlist.php') {
1252
			$col_heading = I18N::translate('Spouses');
1253
		} else {
1254
			$col_heading = I18N::translate('Individuals');
1255
		}
1256
1257
		$html .=
1258
			'<table ' . Datatables::surnameTableAttributes() . '>' .
1259
			'<thead>' .
1260
			'<tr>' .
1261
			'<th>' . I18N::translate('Surname') . '</th>' .
1262
			'<th>' . $col_heading . '</th>' .
1263
			'</tr>' .
1264
			'</thead>';
1265
1266
		$html .= '<tbody>';
1267
		foreach ($surnames as $surn => $surns) {
1268
			// Each surname links back to the indi/fam surname list
1269
			if ($surn) {
1270
				$url = $script . '?surname=' . rawurlencode($surn) . '&amp;ged=' . $tree->getNameUrl();
1271
			} else {
1272
				$url = $script . '?alpha=,&amp;ged=' . $tree->getNameUrl();
1273
			}
1274
			$html .= '<tr>';
1275
			// Surname
1276
			$html .= '<td data-sort="' . e($surn) . '">';
1277
			// Multiple surname variants, e.g. von Groot, van Groot, van der Groot, etc.
1278
			foreach ($surns as $spfxsurn => $indis) {
1279
				if ($spfxsurn) {
1280
					$html .= '<a href="' . $url . '" dir="auto">' . e($spfxsurn) . '</a><br>';
1281
				} else {
1282
					// No surname, but a value from "2 SURN"? A common workaround for toponyms, etc.
1283
					$html .= '<a href="' . $url . '" dir="auto">' . e($surn) . '</a><br>';
1284
				}
1285
			}
1286
			$html .= '</td>';
1287
			// Surname count
1288
			$subtotal = 0;
1289
			foreach ($surns as $indis) {
1290
				$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

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

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

1443
		$table_id = 'table-even-' . /** @scrutinizer ignore-type */ Uuid::uuid4(); // lists requires a unique ID in case there are multiple lists per page
Loading history...
1444
		$controller
1445
			->addInlineJavascript('
1446
				$("#' . $table_id . '").dataTable({
1447
					dom: "t",
1448
					' . I18N::datatablesI18N() . ',
1449
					autoWidth: false,
1450
					paging: false,
1451
					lengthChange: false,
1452
					filter: false,
1453
					info: true,
1454
					sorting: [[ ' . ($sort_by == 'alpha' ? 0 : 1) . ', "asc"]],
1455
					columns: [
1456
						/* Name        */ { type: "text" },
1457
						/* Date        */ { type: "num" },
1458
						/* Anniversary */ { type: "num" },
1459
						/* Event       */ { type: "text" }
1460
					]
1461
				});
1462
			');
1463
1464
		// Did we have any output? Did we skip anything?
1465
		$filter          = 0;
1466
		$filtered_events = [];
1467
1468
		foreach (FunctionsDb::getEventsList($startjd, $endjd, $events, $WT_TREE) as $fact) {
1469
			$record = $fact->getParent();
1470
			// Only living people ?
1471
			if ($only_living) {
1472
				if ($record instanceof Individual && $record->isDead()) {
1473
					$filter++;
1474
					continue;
1475
				}
1476
				if ($record instanceof Family) {
1477
					$husb = $record->getHusband();
1478
					if (is_null($husb) || $husb->isDead()) {
1479
						$filter++;
1480
						continue;
1481
					}
1482
					$wife = $record->getWife();
1483
					if (is_null($wife) || $wife->isDead()) {
1484
						$filter++;
1485
						continue;
1486
					}
1487
				}
1488
			}
1489
1490
			$filtered_events[] = $fact;
1491
		}
1492
1493
		if (!empty($filtered_events)) {
1494
			$html .= '<table id="' . $table_id . '" class="width100">';
1495
			$html .= '<thead><tr>';
1496
			$html .= '<th>' . I18N::translate('Record') . '</th>';
1497
			$html .= '<th>' . GedcomTag::getLabel('DATE') . '</th>';
1498
			$html .= '<th><i class="icon-reminder" title="' . I18N::translate('Anniversary') . '"></i></th>';
1499
			$html .= '<th>' . GedcomTag::getLabel('EVEN') . '</th>';
1500
			$html .= '</tr></thead><tbody>';
1501
1502
			foreach ($filtered_events as $n => $fact) {
1503
				$record = $fact->getParent();
1504
				$html .= '<tr>';
1505
				$html .= '<td data-sort="' . e($record->getSortName()) . '">';
1506
				$html .= '<a href="' . e($record->url()) . '">' . $record->getFullName() . '</a>';
1507
				if ($record instanceof Individual) {
1508
					$html .= $record->getSexImage();
1509
				}
1510
				$html .= '</td>';
1511
				$html .= '<td data-sort="' . $fact->jd . '">';
1512
				$html .= $fact->getDate()->display();
1513
				$html .= '</td>';
1514
				$html .= '<td class="center" data-sort="' . $fact->anniv . '">';
1515
				$html .= ($fact->anniv ? I18N::number($fact->anniv) : '');
1516
				$html .= '</td>';
1517
				$html .= '<td class="center">' . $fact->getLabel() . '</td>';
1518
				$html .= '</tr>';
1519
			}
1520
1521
			$html .= '</tbody></table>';
1522
		} else {
1523
			if ($endjd === WT_CLIENT_JD) {
1524
				// We're dealing with the Today’s Events block
1525
				if ($filter === 0) {
0 ignored issues
show
introduced by
The condition $filter === 0 can never be false.
Loading history...
1526
					$html .= I18N::translate('No events exist for today.');
1527
				} else {
1528
					$html .= I18N::translate('No events for living individuals exist for today.');
1529
				}
1530
			} else {
1531
				// We're dealing with the Upcoming Events block
1532
				if ($filter === 0) {
0 ignored issues
show
introduced by
The condition $filter === 0 can never be false.
Loading history...
1533
					if ($endjd === $startjd) {
1534
						$html .= I18N::translate('No events exist for tomorrow.');
1535
					} else {
1536
						$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));
1537
					}
1538
				} else {
1539
					if ($endjd === $startjd) {
1540
						$html .= I18N::translate('No events for living individuals exist for tomorrow.');
1541
					} else {
1542
						// I18N: translation for %s==1 is unused; it is translated separately as “tomorrow”
1543
						$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));
1544
					}
1545
				}
1546
			}
1547
		}
1548
1549
		return $html;
1550
	}
1551
1552
	/**
1553
	 * Print a list of events
1554
	 *
1555
	 * This performs the same function as print_events_table(), but formats the output differently.
1556
	 *
1557
	 * @param int $startjd
1558
	 * @param int $endjd
1559
	 * @param string $events
1560
	 * @param bool $only_living
1561
	 * @param string $sort_by
1562
	 *
1563
	 * @return string
1564
	 */
1565
	public static function eventsList($startjd, $endjd, $events = 'BIRT MARR DEAT', $only_living = false, $sort_by = 'anniv') {
1566
		global $WT_TREE;
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...
1567
1568
		// Did we have any output? Did we skip anything?
1569
		$output          = 0;
1570
		$filter          = 0;
1571
		$filtered_events = [];
1572
		$html            = '';
1573
		foreach (FunctionsDb::getEventsList($startjd, $endjd, $events, $WT_TREE) as $fact) {
1574
			$record = $fact->getParent();
1575
			// only living people ?
1576
			if ($only_living) {
1577
				if ($record instanceof Individual && $record->isDead()) {
1578
					$filter++;
1579
					continue;
1580
				}
1581
				if ($record instanceof Family) {
1582
					$husb = $record->getHusband();
1583
					if (is_null($husb) || $husb->isDead()) {
1584
						$filter++;
1585
						continue;
1586
					}
1587
					$wife = $record->getWife();
1588
					if (is_null($wife) || $wife->isDead()) {
1589
						$filter++;
1590
						continue;
1591
					}
1592
				}
1593
			}
1594
1595
			$output++;
1596
1597
			$filtered_events[] = $fact;
1598
		}
1599
1600
		// Now we've filtered the list, we can sort by event, if required
1601
		switch ($sort_by) {
1602
			case 'anniv':
1603
				// Data is already sorted by anniversary date
1604
				break;
1605
			case 'alpha':
1606
				uasort($filtered_events, function (Fact $x, Fact $y) {
1607
					return GedcomRecord::compare($x->getParent(), $y->getParent());
1608
				});
1609
				break;
1610
		}
1611
1612
		foreach ($filtered_events as $fact) {
1613
			$record = $fact->getParent();
1614
			$html .= '<a href="' . e($record->url()) . '" class="list_item name2">' . $record->getFullName() . '</a>';
1615
			if ($record instanceof Individual) {
1616
				$html .= $record->getSexImage();
1617
			}
1618
			$html .= '<br><div class="indent">';
1619
			$html .= $fact->getLabel() . ' — ' . $fact->getDate()->display(true);
1620
			if ($fact->anniv) {
1621
				$html .= ' (' . I18N::translate('%s year anniversary', I18N::number($fact->anniv)) . ')';
1622
			}
1623
			if (!$fact->getPlace()->isEmpty()) {
1624
				$html .= ' — <a href="' . $fact->getPlace()->getURL() . '">' . $fact->getPlace()->getFullName() . '</a>';
1625
			}
1626
			$html .= '</div>';
1627
		}
1628
1629
		// Print a final summary message about restricted/filtered facts
1630
		$summary = '';
1631
		if ($endjd == WT_CLIENT_JD) {
1632
			// We're dealing with the Today’s Events block
1633
			if ($output == 0) {
0 ignored issues
show
introduced by
The condition $output == 0 can never be false.
Loading history...
1634
				if ($filter == 0) {
0 ignored issues
show
introduced by
The condition $filter == 0 can never be false.
Loading history...
1635
					$summary = I18N::translate('No events exist for today.');
1636
				} else {
1637
					$summary = I18N::translate('No events for living individuals exist for today.');
1638
				}
1639
			}
1640
		} else {
1641
			// We're dealing with the Upcoming Events block
1642
			if ($output == 0) {
0 ignored issues
show
introduced by
The condition $output == 0 can never be false.
Loading history...
1643
				if ($filter == 0) {
0 ignored issues
show
introduced by
The condition $filter == 0 can never be false.
Loading history...
1644
					if ($endjd == $startjd) {
1645
						$summary = I18N::translate('No events exist for tomorrow.');
1646
					} else {
1647
						// I18N: translation for %s==1 is unused; it is translated separately as “tomorrow”
1648
						$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));
1649
					}
1650
				} else {
1651
					if ($endjd == $startjd) {
1652
						$summary = I18N::translate('No events for living individuals exist for tomorrow.');
1653
					} else {
1654
						// I18N: translation for %s==1 is unused; it is translated separately as “tomorrow”
1655
						$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));
1656
					}
1657
				}
1658
			}
1659
		}
1660
		if ($summary) {
1661
			$html .= '<b>' . $summary . '</b>';
1662
		}
1663
1664
		return $html;
1665
	}
1666
1667
	/**
1668
	 * Print a chart by age using Google chart API
1669
	 *
1670
	 * @param int[] $data
1671
	 * @param string $title
1672
	 *
1673
	 * @return string
1674
	 */
1675
	public static function chartByAge($data, $title) {
1676
		$count  = 0;
1677
		$agemax = 0;
1678
		$vmax   = 0;
1679
		$avg    = 0;
1680
		foreach ($data as $age => $v) {
1681
			$n      = strlen($v);
1682
			$vmax   = max($vmax, $n);
1683
			$agemax = max($agemax, $age);
1684
			$count += $n;
1685
			$avg += $age * $n;
1686
		}
1687
		if ($count < 1) {
1688
			return '';
1689
		}
1690
		$avg       = round($avg / $count);
1691
		$chart_url = 'https://chart.googleapis.com/chart?cht=bvs'; // chart type
1692
		$chart_url .= '&amp;chs=725x150'; // size
1693
		$chart_url .= '&amp;chbh=3,2,2'; // bvg : 4,1,2
0 ignored issues
show
Unused Code Comprehensibility introduced by
60% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
1694
		$chart_url .= '&amp;chf=bg,s,FFFFFF99'; //background color
1695
		$chart_url .= '&amp;chco=0000FF,FFA0CB,FF0000'; // bar color
1696
		$chart_url .= '&amp;chdl=' . rawurlencode(I18N::translate('Males')) . '|' . rawurlencode(I18N::translate('Females')) . '|' . rawurlencode(I18N::translate('Average age') . ': ' . $avg); // legend & average age
1697
		$chart_url .= '&amp;chtt=' . rawurlencode($title); // title
1698
		$chart_url .= '&amp;chxt=x,y,r'; // axis labels specification
1699
		$chart_url .= '&amp;chm=V,FF0000,0,' . ($avg - 0.3) . ',1'; // average age line marker
1700
		$chart_url .= '&amp;chxl=0:|'; // label
1701
		for ($age = 0; $age <= $agemax; $age += 5) {
1702
			$chart_url .= $age . '|||||'; // x axis
1703
		}
1704
		$chart_url .= '|1:||' . rawurlencode(I18N::percentage($vmax / $count)); // y axis
1705
		$chart_url .= '|2:||';
1706
		$step = $vmax;
1707
		for ($d = $vmax; $d > 0; $d--) {
1708
			if ($vmax < ($d * 10 + 1) && ($vmax % $d) == 0) {
1709
				$step = $d;
1710
			}
1711
		}
1712
		if ($step == $vmax) {
1713
			for ($d = $vmax - 1; $d > 0; $d--) {
1714
				if (($vmax - 1) < ($d * 10 + 1) && (($vmax - 1) % $d) == 0) {
1715
					$step = $d;
1716
				}
1717
			}
1718
		}
1719
		for ($n = $step; $n < $vmax; $n += $step) {
1720
			$chart_url .= $n . '|';
1721
		}
1722
		$chart_url .= rawurlencode($vmax . ' / ' . $count); // r axis
1723
		$chart_url .= '&amp;chg=100,' . round(100 * $step / $vmax, 1) . ',1,5'; // grid
1724
		$chart_url .= '&amp;chd=s:'; // data : simple encoding from A=0 to 9=61
1725
		$CHART_ENCODING61 = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
1726
		for ($age = 0; $age <= $agemax; $age++) {
1727
			$chart_url .= $CHART_ENCODING61[(int) (substr_count($data[$age], 'M') * 61 / $vmax)];
1728
		}
1729
		$chart_url .= ',';
1730
		for ($age = 0; $age <= $agemax; $age++) {
1731
			$chart_url .= $CHART_ENCODING61[(int) (substr_count($data[$age], 'F') * 61 / $vmax)];
1732
		}
1733
		$html = '<img src="' . $chart_url . '" alt="' . $title . '" title="' . $title . '" class="gchart">';
1734
1735
		return $html;
1736
	}
1737
1738
	/**
1739
	 * Print a chart by decade using Google chart API
1740
	 *
1741
	 * @param int[] $data
1742
	 * @param string $title
1743
	 *
1744
	 * @return string
1745
	 */
1746
	public static function chartByDecade($data, $title) {
1747
		$count = 0;
1748
		$vmax  = 0;
1749
		foreach ($data as $v) {
1750
			$n    = strlen($v);
1751
			$vmax = max($vmax, $n);
1752
			$count += $n;
1753
		}
1754
		if ($count < 1) {
1755
			return '';
1756
		}
1757
		$chart_url = 'https://chart.googleapis.com/chart?cht=bvs'; // chart type
1758
		$chart_url .= '&amp;chs=360x150'; // size
1759
		$chart_url .= '&amp;chbh=3,3'; // bvg : 4,1,2
0 ignored issues
show
Unused Code Comprehensibility introduced by
60% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
1760
		$chart_url .= '&amp;chf=bg,s,FFFFFF99'; //background color
1761
		$chart_url .= '&amp;chco=0000FF,FFA0CB'; // bar color
1762
		$chart_url .= '&amp;chtt=' . rawurlencode($title); // title
1763
		$chart_url .= '&amp;chxt=x,y,r'; // axis labels specification
1764
		$chart_url .= '&amp;chxl=0:|&lt;|||'; // <1570
1765
		for ($y = 1600; $y < 2030; $y += 50) {
1766
			$chart_url .= $y . '|||||'; // x axis
1767
		}
1768
		$chart_url .= '|1:||' . rawurlencode(I18N::percentage($vmax / $count)); // y axis
1769
		$chart_url .= '|2:||';
1770
		$step = $vmax;
1771
		for ($d = $vmax; $d > 0; $d--) {
1772
			if ($vmax < ($d * 10 + 1) && ($vmax % $d) == 0) {
1773
				$step = $d;
1774
			}
1775
		}
1776
		if ($step == $vmax) {
1777
			for ($d = $vmax - 1; $d > 0; $d--) {
1778
				if (($vmax - 1) < ($d * 10 + 1) && (($vmax - 1) % $d) == 0) {
1779
					$step = $d;
1780
				}
1781
			}
1782
		}
1783
		for ($n = $step; $n < $vmax; $n += $step) {
1784
			$chart_url .= $n . '|';
1785
		}
1786
		$chart_url .= rawurlencode($vmax . ' / ' . $count); // r axis
1787
		$chart_url .= '&amp;chg=100,' . round(100 * $step / $vmax, 1) . ',1,5'; // grid
1788
		$chart_url .= '&amp;chd=s:'; // data : simple encoding from A=0 to 9=61
1789
		$CHART_ENCODING61 = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
1790
		for ($y = 1570; $y < 2030; $y += 10) {
1791
			$chart_url .= $CHART_ENCODING61[(int) (substr_count($data[$y], 'M') * 61 / $vmax)];
1792
		}
1793
		$chart_url .= ',';
1794
		for ($y = 1570; $y < 2030; $y += 10) {
1795
			$chart_url .= $CHART_ENCODING61[(int) (substr_count($data[$y], 'F') * 61 / $vmax)];
1796
		}
1797
		$html = '<img src="' . $chart_url . '" alt="' . $title . '" title="' . $title . '" class="gchart">';
1798
1799
		return $html;
1800
	}
1801
}
1802