Passed
Branch master (380e00)
by Greg
20:17
created

FunctionsPrintLists::surnameTable()   C

Complexity

Conditions 9
Paths 98

Size

Total Lines 58
Code Lines 40

Duplication

Lines 5
Ratio 8.62 %

Importance

Changes 0
Metric Value
cc 9
eloc 40
nc 98
nop 3
dl 5
loc 58
rs 6.9928
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) 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;
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
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>';
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')) . '
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')) . '
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
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>
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>
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>';
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>';
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>';
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
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) {
0 ignored issues
show
Bug introduced by
The expression $media_object of type object<Fisharebest\Webtrees\Media> is not traversable.
Loading history...
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 seem to exist on object<Fisharebest\Webtrees\Media>.

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);
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));
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
0 ignored issues
show
Documentation introduced by
Should the return type not be string|null?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
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) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $first_spfxsurn of type null|integer is loosely compared to true; this is ambiguous if the integer can be zero. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
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;
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...
1444
1445
		$html     = '';
1446
		$table_id = 'table-even-' . Uuid::uuid4(); // lists requires a unique ID in case there are multiple lists per page
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;
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...
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
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...
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
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...
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