FunctionsPrintLists::surnameList()   F
last analyzed

Complexity

Conditions 17
Paths 255

Size

Total Lines 66
Code Lines 48

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 17
eloc 48
nc 255
nop 5
dl 0
loc 66
rs 3.6958
c 0
b 0
f 0

How to fix   Long Method    Complexity   

Long Method

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

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

Commonly applied refactorings include:

1
<?php
2
/**
3
 * webtrees: online genealogy
4
 * Copyright (C) 2019 webtrees development team
5
 * This program is free software: you can redistribute it and/or modify
6
 * it under the terms of the GNU General Public License as published by
7
 * the Free Software Foundation, either version 3 of the License, or
8
 * (at your option) any later version.
9
 * This program is distributed in the hope that it will be useful,
10
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12
 * GNU General Public License for more details.
13
 * You should have received a copy of the GNU General Public License
14
 * along with this program. If not, see <http://www.gnu.org/licenses/>.
15
 */
16
namespace Fisharebest\Webtrees\Functions;
17
18
use Fisharebest\Webtrees\Auth;
19
use Fisharebest\Webtrees\Database;
20
use Fisharebest\Webtrees\Date;
21
use Fisharebest\Webtrees\Fact;
22
use Fisharebest\Webtrees\Family;
23
use Fisharebest\Webtrees\Filter;
24
use Fisharebest\Webtrees\GedcomRecord;
25
use Fisharebest\Webtrees\GedcomTag;
26
use Fisharebest\Webtrees\I18N;
27
use Fisharebest\Webtrees\Individual;
28
use Fisharebest\Webtrees\Media;
29
use Fisharebest\Webtrees\Note;
30
use Fisharebest\Webtrees\Place;
31
use Fisharebest\Webtrees\Repository;
32
use Fisharebest\Webtrees\Source;
33
use Fisharebest\Webtrees\Tree;
34
use Rhumsaa\Uuid\Uuid;
35
36
/**
37
 * Class FunctionsPrintLists - create sortable lists using datatables.net
38
 */
39
class FunctionsPrintLists
40
{
41
    /**
42
     * Generate a SURN,GIVN and GIVN,SURN sortable name for an individual.
43
     * This allows table data to sort by surname or given names.
44
     *
45
     * Use AAAA as a separator (instead of ","), as Javascript localeCompare()
46
     * ignores punctuation and "ANN,ROACH" would sort after "ANNE,ROACH",
47
     * instead of before it.
48
     *
49
     * @param Individual $individual
50
     *
51
     * @return string[]
52
     */
53
    private static function sortableNames(Individual $individual)
54
    {
55
        $names   = $individual->getAllNames();
56
        $primary = $individual->getPrimaryName();
57
58
        list($surn, $givn) = explode(',', $names[$primary]['sort']);
59
60
        $givn = str_replace('@P.N.', 'AAAA', $givn);
61
        $surn = str_replace('@N.N.', 'AAAA', $surn);
62
63
        return array(
64
            $surn . 'AAAA' . $givn,
65
            $givn . 'AAAA' . $surn,
66
        );
67
    }
68
69
    /**
70
     * Print a table of individuals
71
     *
72
     * @param Individual[] $indiviudals
73
     * @param string       $option
74
     *
75
     * @return string
76
     */
77
    public static function individualTable($indiviudals, $option = '')
78
    {
79
        global $controller, $WT_TREE;
80
81
        $table_id = 'table-indi-' . Uuid::uuid4(); // lists requires a unique ID in case there are multiple lists per page
82
83
        $controller
84
            ->addExternalJavascript(WT_JQUERY_DATATABLES_JS_URL)
85
            ->addInlineJavascript('
86
				jQuery.fn.dataTableExt.oSort["text-asc"] = textCompareAsc;
87
				jQuery.fn.dataTableExt.oSort["text-desc"] = textCompareDesc;
88
				jQuery("#' . $table_id . '").dataTable( {
89
					dom: \'<"H"<"filtersH_' . $table_id . '">T<"dt-clear">pf<"dt-clear">irl>t<"F"pl<"dt-clear"><"filtersF_' . $table_id . '">>\',
90
					' . I18N::datatablesI18N() . ',
91
					jQueryUI: true,
92
					autoWidth: false,
93
					processing: true,
94
					retrieve: true,
95
					columns: [
96
						/* Given names  */ { type: "text" },
97
						/* Surnames     */ { type: "text" },
98
						/* SOSA numnber */ { type: "num", visible: ' . ($option === 'sosa' ? 'true' : 'false') . ' },
99
						/* Birth date   */ { type: "num" },
100
						/* Anniversary  */ { type: "num" },
101
						/* Birthplace   */ { type: "text" },
102
						/* Children     */ { type: "num" },
103
						/* Deate date   */ { type: "num" },
104
						/* Anniversary  */ { type: "num" },
105
						/* Age          */ { type: "num" },
106
						/* Death place  */ { type: "text" },
107
						/* Last change  */ { visible: ' . ($WT_TREE->getPreference('SHOW_LAST_CHANGE') ? 'true' : 'false') . ' },
108
						/* Filter sex   */ { sortable: false },
109
						/* Filter birth */ { sortable: false },
110
						/* Filter death */ { sortable: false },
111
						/* Filter tree  */ { sortable: false }
112
					],
113
					sorting: [[' . ($option === 'sosa' ? '4, "asc"' : '1, "asc"') . ']],
114
					displayLength: 20,
115
					pagingType: "full_numbers"
116
				});
117
118
				jQuery("#' . $table_id . '")
119
				/* Hide/show parents */
120
				.on("click", ".btn-toggle-parents", function() {
121
					jQuery(this).toggleClass("ui-state-active");
122
					jQuery(".parents", jQuery(this).closest("table").DataTable().rows().nodes()).slideToggle();
123
				})
124
				/* Hide/show statistics */
125
				.on("click", ".btn-toggle-statistics", function() {
126
					jQuery(this).toggleClass("ui-state-active");
127
					jQuery("#indi_list_table-charts_' . $table_id . '").slideToggle();
128
				})
129
				/* Filter buttons in table header */
130
				.on("click", "button[data-filter-column]", function() {
131
					var btn = jQuery(this);
132
					// De-activate the other buttons in this button group
133
					btn.siblings().removeClass("ui-state-active");
134
					// Apply (or clear) this filter
135
					var col = jQuery("#' . $table_id . '").DataTable().column(btn.data("filter-column"));
136
					if (btn.hasClass("ui-state-active")) {
137
						btn.removeClass("ui-state-active");
138
						col.search("").draw();
139
					} else {
140
						btn.addClass("ui-state-active");
141
						col.search(btn.data("filter-value")).draw();
142
					}
143
				});
144
145
				jQuery(".indi-list").css("visibility", "visible");
146
				jQuery(".loading-image").css("display", "none");
147
			');
148
149
        $max_age = (int) $WT_TREE->getPreference('MAX_ALIVE_AGE');
150
151
        // Inititialise chart data
152
        $deat_by_age = array();
153
        for ($age = 0; $age <= $max_age; $age++) {
154
            $deat_by_age[$age] = '';
155
        }
156
        $birt_by_decade = array();
157
        $deat_by_decade = array();
158
        for ($year = 1550; $year < 2030; $year += 10) {
159
            $birt_by_decade[$year] = '';
160
            $deat_by_decade[$year] = '';
161
        }
162
163
        $html = '
164
			<div class="loading-image"></div>
165
			<div class="indi-list">
166
				<table id="' . $table_id . '">
167
					<thead>
168
						<tr>
169
							<th colspan="16">
170
								<div class="btn-toolbar">
171
									<div class="btn-group">
172
										<button
173
											class="ui-state-default"
174
											data-filter-column="12"
175
											data-filter-value="M"
176
											title="' . I18N::translate('Show only males.') . '"
177
											type="button"
178
										>
179
										  ' . Individual::sexImage('M', 'large') . '
180
										</button>
181
										<button
182
											class="ui-state-default"
183
											data-filter-column="12"
184
											data-filter-value="F"
185
											title="' . I18N::translate('Show only females.') . '"
186
											type="button"
187
										>
188
											' . Individual::sexImage('F', 'large') . '
189
										</button>
190
										<button
191
											class="ui-state-default"
192
											data-filter-column="12"
193
											data-filter-value="U"
194
											title="' . I18N::translate('Show only individuals for whom the gender is not known.') . '"
195
											type="button"
196
										>
197
											' . Individual::sexImage('U', 'large') . '
198
										</button>
199
									</div>
200
									<div class="btn-group">
201
										<button
202
											class="ui-state-default"
203
											data-filter-column="14"
204
											data-filter-value="N"
205
											title="' . I18N::translate('Show individuals who are alive or couples where both partners are alive.') . '"
206
											type="button"
207
										>
208
											' . I18N::translate('Alive') . '
209
										</button>
210
										<button
211
											class="ui-state-default"
212
											data-filter-column="14"
213
											data-filter-value="Y"
214
											title="' . I18N::translate('Show individuals who are dead or couples where both partners are dead.') . '"
215
											type="button"
216
										>
217
											' . I18N::translate('Dead') . '
218
										</button>
219
										<button
220
											class="ui-state-default"
221
											data-filter-column="14"
222
											data-filter-value="YES"
223
											title="' . I18N::translate('Show individuals who died more than 100 years ago.') . '"
224
											type="button"
225
										>
226
											' . GedcomTag::getLabel('DEAT') . '&gt;100
227
										</button>
228
										<button
229
											class="ui-state-default"
230
											data-filter-column="14"
231
											data-filter-value="Y100"
232
											title="' . I18N::translate('Show individuals who died within the last 100 years.') . '"
233
											type="button"
234
										>
235
											' . GedcomTag::getLabel('DEAT') . '&lt;=100
236
										</button>
237
									</div>
238
									<div class="btn-group">
239
										<button
240
											class="ui-state-default"
241
											data-filter-column="13"
242
											data-filter-value="YES"
243
											title="' . I18N::translate('Show individuals born more than 100 years ago.') . '"
244
											type="button"
245
										>
246
											' . GedcomTag::getLabel('BIRT') . '&gt;100
247
										</button>
248
										<button
249
											class="ui-state-default"
250
											data-filter-column="13"
251
											data-filter-value="Y100"
252
											title="' . I18N::translate('Show individuals born within the last 100 years.') . '"
253
											type="button"
254
										>
255
											' . GedcomTag::getLabel('BIRT') . '&lt;=100
256
										</button>
257
									</div>
258
									<div class="btn-group">
259
										<button
260
											class="ui-state-default"
261
											data-filter-column="15"
262
											data-filter-value="R"
263
											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.') . '"
264
											type="button"
265
										>
266
											' . I18N::translate('Roots') . '
267
										</button>
268
										<button
269
											class="ui-state-default"
270
											data-filter-column="15"
271
											data-filter-value="L"
272
											title="' . I18N::translate('Show “leaves” couples or individuals. These are individuals who are alive but have no children recorded in the database.') . '"
273
											type="button"
274
										>
275
											' . I18N::translate('Leaves') . '
276
										</button>
277
									</div>
278
								</div>
279
							</th>
280
						</tr>
281
						<tr>
282
							<th>' . GedcomTag::getLabel('GIVN') . '</th>
283
							<th>' . GedcomTag::getLabel('SURN') . '</th>
284
							<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>
285
							<th>' . GedcomTag::getLabel('BIRT') . '</th>
286
							<th><i class="icon-reminder" title="' . I18N::translate('Anniversary') . '"></i></th>
287
							<th>' . GedcomTag::getLabel('PLAC') . '</th>
288
							<th><i class="icon-children" title="' . I18N::translate('Children') . '"></i></th>
289
							<th>' . GedcomTag::getLabel('DEAT') . '</th>
290
							<th><i class="icon-reminder" title="' . I18N::translate('Anniversary') . '"></i></th>
291
							<th>' . GedcomTag::getLabel('AGE') . '</th>
292
							<th>' . GedcomTag::getLabel('PLAC') . '</th>
293
							<th>' . GedcomTag::getLabel('CHAN') . '</th>
294
							<th hidden></th>
295
							<th hidden></th>
296
							<th hidden></th>
297
							<th hidden></th>
298
						</tr>
299
					</thead>
300
					<tfoot>
301
						<tr>
302
							<th colspan="16">
303
								<div class="btn-toolbar">
304
									<div class="btn-group">
305
										<button type="button" class="ui-state-default btn-toggle-parents">
306
											' . I18N::translate('Show parents') . '
307
										</button>
308
										<button type="button" class="ui-state-default btn-toggle-statistics">
309
											' . I18N::translate('Show statistics charts') . '
310
										</button>
311
									</div>
312
								</div>
313
							</th>
314
						</tr>
315
					</tfoot>
316
					<tbody>';
317
318
        $hundred_years_ago = new Date(date('Y') - 100);
319
        $unique_indis      = array(); // Don't double-count indis with multiple names.
320
321
        foreach ($indiviudals as $key => $individual) {
322
            if (!$individual->canShowName()) {
323
                continue;
324
            }
325
            if ($individual->isPendingAddtion()) {
326
                $class = ' class="new"';
327
            } elseif ($individual->isPendingDeletion()) {
328
                $class = ' class="old"';
329
            } else {
330
                $class = '';
331
            }
332
            $html .= '<tr' . $class . '>';
333
            // Extract Given names and Surnames for sorting
334
            list($surn_givn, $givn_surn) = self::sortableNames($individual);
335
336
            $html .= '<td colspan="2" data-sort="' . Filter::escapeHtml($givn_surn) . '">';
337
            foreach ($individual->getAllNames() as $num => $name) {
338
                if ($name['type'] == 'NAME') {
339
                    $title = '';
340
                } else {
341
                    $title = 'title="' . strip_tags(GedcomTag::getLabel($name['type'], $individual)) . '"';
342
                }
343
                if ($num == $individual->getPrimaryName()) {
344
                    $class             = ' class="name2"';
345
                    $sex_image         = $individual->getSexImage();
346
                } else {
347
                    $class     = '';
348
                    $sex_image = '';
349
                }
350
                $html .= '<a ' . $title . ' href="' . $individual->getHtmlUrl() . '"' . $class . '>' . FunctionsPrint::highlightSearchHits($name['full']) . '</a>' . $sex_image . '<br>';
351
            }
352
            $html .= $individual->getPrimaryParentsNames('parents details1', 'none');
353
            $html .= '</td>';
354
355
            // Hidden column for sortable name
356
            $html .= '<td hidden data-sort="' . Filter::escapeHtml($surn_givn) . '"></td>';
357
358
            // SOSA
359
            $html .= '<td class="center" data-sort="' . $key . '">';
360
            if ($option === 'sosa') {
361
                $html .= '<a href="relationship.php?pid1=' . $indiviudals[1] . '&amp;pid2=' . $individual->getXref() . '" title="' . I18N::translate('Relationships') . '">' . I18N::number($key) . '</a>';
362
            }
363
            $html .= '</td>';
364
365
            // Birth date
366
            $birth_dates = $individual->getAllBirthDates();
367
            $html .= '<td data-sort="' . $individual->getEstimatedBirthDate()->julianDay() . '">';
368
            foreach ($birth_dates as $n => $birth_date) {
369
                if ($n > 0) {
370
                    $html .= '<br>';
371
                }
372
                $html .= $birth_date->display(true);
373
            }
374
            $html .= '</td>';
375
376
            // Birth anniversary
377
            if (isset($birth_dates[0]) && $birth_dates[0]->gregorianYear() >= 1550 && $birth_dates[0]->gregorianYear() < 2030 && !isset($unique_indis[$individual->getXref()])) {
378
                $birt_by_decade[(int) ($birth_dates[0]->gregorianYear() / 10) * 10] .= $individual->getSex();
379
                $anniversary = Date::getAge($birth_dates[0], null, 2);
380
            } else {
381
                $anniversary = '';
382
            }
383
            $html .= '<td class="center" data-sort="' . -$individual->getEstimatedBirthDate()->julianDay() . '">' . $anniversary . '</td>';
384
385
            // Birth place
386
            $html .= '<td>';
387
            foreach ($individual->getAllBirthPlaces() as $n => $birth_place) {
388
                $tmp = new Place($birth_place, $individual->getTree());
389
                if ($n > 0) {
390
                    $html .= '<br>';
391
                }
392
                $html .= '<a href="' . $tmp->getURL() . '" title="' . strip_tags($tmp->getFullName()) . '">';
393
                $html .= FunctionsPrint::highlightSearchHits($tmp->getShortName()) . '</a>';
394
            }
395
            $html .= '</td>';
396
397
            // Number of children
398
            $number_of_children = $individual->getNumberOfChildren();
399
            $html .= '<td class="center" data-sort="' . $number_of_children . '">' . I18N::number($number_of_children) . '</td>';
400
401
            // Death date
402
            $death_dates = $individual->getAllDeathDates();
403
            $html .= '<td data-sort="' . $individual->getEstimatedDeathDate()->julianDay() . '">';
404
            foreach ($death_dates as $num => $death_date) {
405
                if ($num) {
406
                    $html .= '<br>';
407
                }
408
                $html .= $death_date->display(true);
409
            }
410
            $html .= '</td>';
411
412
            // Death anniversary
413
            if (isset($death_dates[0]) && $death_dates[0]->gregorianYear() >= 1550 && $death_dates[0]->gregorianYear() < 2030 && !isset($unique_indis[$individual->getXref()])) {
414
                $birt_by_decade[(int) ($death_dates[0]->gregorianYear() / 10) * 10] .= $individual->getSex();
415
                $anniversary = Date::getAge($death_dates[0], null, 2);
416
            } else {
417
                $anniversary = '';
418
            }
419
            $html .= '<td class="center" data-sort="' . -$individual->getEstimatedDeathDate()->julianDay() . '">' . $anniversary . '</td>';
420
421
            // Age at death
422
            if (isset($birth_dates[0]) && isset($death_dates[0])) {
423
                $age_at_death      = Date::getAge($birth_dates[0], $death_dates[0], 0);
424
                $age_at_death_sort = Date::getAge($birth_dates[0], $death_dates[0], 2);
425
                if (!isset($unique_indis[$individual->getXref()]) && $age >= 0 && $age <= $max_age) {
426
                    $deat_by_age[$age_at_death] .= $individual->getSex();
427
                }
428
            } else {
429
                $age_at_death      = '';
430
                $age_at_death_sort = PHP_INT_MAX;
431
            }
432
            $html .= '<td class="center" data-sort="' . $age_at_death_sort . '">' . $age_at_death . '</td>';
433
434
            // Death place
435
            $html .= '<td>';
436
            foreach ($individual->getAllDeathPlaces() as $n => $death_place) {
437
                $tmp = new Place($death_place, $individual->getTree());
438
                if ($n > 0) {
439
                    $html .= '<br>';
440
                }
441
                $html .= '<a href="' . $tmp->getURL() . '" title="' . strip_tags($tmp->getFullName()) . '">';
442
                $html .= FunctionsPrint::highlightSearchHits($tmp->getShortName()) . '</a>';
443
            }
444
            $html .= '</td>';
445
446
            // Last change
447
            $html .= '<td data-sort="' . $individual->lastChangeTimestamp(true) . '">' . $individual->lastChangeTimestamp() . '</td>';
448
449
            // Filter by sex
450
            $html .= '<td hidden>' . $individual->getSex() . '</td>';
451
452
            // Filter by birth date
453
            $html .= '<td hidden>';
454
            if (!$individual->canShow() || Date::compare($individual->getEstimatedBirthDate(), $hundred_years_ago) > 0) {
455
                $html .= 'Y100';
456
            } else {
457
                $html .= 'YES';
458
            }
459
            $html .= '</td>';
460
461
            // Filter by death date
462
            $html .= '<td hidden>';
463
            // Died in last 100 years? Died? Not dead?
464
            if (isset($death_dates[0]) && Date::compare($death_dates[0], $hundred_years_ago) > 0) {
465
                $html .= 'Y100';
466
            } elseif ($individual->isDead()) {
467
                $html .= 'YES';
468
            } else {
469
                $html .= 'N';
470
            }
471
            $html .= '</td>';
472
473
            // Filter by roots/leaves
474
            $html .= '<td hidden>';
475
            if (!$individual->getChildFamilies()) {
476
                $html .= 'R';
477
            } elseif (!$individual->isDead() && $individual->getNumberOfChildren() < 1) {
478
                $html .= 'L';
479
                $html .= '&nbsp;';
480
            }
481
            $html .= '</td>';
482
            $html .= '</tr>';
483
484
            $unique_indis[$individual->getXref()] = true;
485
        }
486
        $html .= '
487
					</tbody>
488
				</table>
489
				<div id="indi_list_table-charts_' . $table_id . '" style="display:none">
490
					<table class="list-charts">
491
						<tr>
492
							<td>
493
								' . self::chartByDecade($birt_by_decade, I18N::translate('Decade of birth')) . '
494
							</td>
495
							<td>
496
								' . self::chartByDecade($deat_by_decade, I18N::translate('Decade of death')) . '
497
							</td>
498
						</tr>
499
						<tr>
500
							<td colspan="2">
501
								' . self::chartByAge($deat_by_age, I18N::translate('Age related to death year')) . '
502
							</td>
503
						</tr>
504
					</table>
505
				</div>
506
			</div>';
507
508
        return $html;
509
    }
510
511
    /**
512
     * Print a table of families
513
     *
514
     * @param Family[] $families
515
     *
516
     * @return string
517
     */
518
    public static function familyTable($families)
519
    {
520
        global $WT_TREE, $controller;
521
522
        $table_id = 'table-fam-' . Uuid::uuid4(); // lists requires a unique ID in case there are multiple lists per page
523
524
        $controller
525
            ->addExternalJavascript(WT_JQUERY_DATATABLES_JS_URL)
526
            ->addInlineJavascript('
527
				jQuery.fn.dataTableExt.oSort["text-asc"] = textCompareAsc;
528
				jQuery.fn.dataTableExt.oSort["text-desc"] = textCompareDesc;
529
				jQuery("#' . $table_id . '").dataTable( {
530
					dom: \'<"H"<"filtersH_' . $table_id . '"><"dt-clear">pf<"dt-clear">irl>t<"F"pl<"dt-clear"><"filtersF_' . $table_id . '">>\',
531
					' . I18N::datatablesI18N() . ',
532
					jQueryUI: true,
533
					autoWidth: false,
534
					processing: true,
535
					retrieve: true,
536
					columns: [
537
						/* Given names         */ { type: "text" },
538
						/* Surnames            */ { type: "text" },
539
						/* Age                 */ { type: "num" },
540
						/* Given names         */ { type: "text" },
541
						/* Surnames            */ { type: "text" },
542
						/* Age                 */ { type: "num" },
543
						/* Marriage date       */ { type: "num" },
544
						/* Anniversary         */ { type: "num" },
545
						/* Marriage place      */ { type: "text" },
546
						/* Children            */ { type: "num" },
547
						/* Last change         */ { visible: ' . ($WT_TREE->getPreference('SHOW_LAST_CHANGE') ? 'true' : 'false') . ' },
548
						/* Filter marriage     */ { sortable: false },
549
						/* Filter alive/dead   */ { sortable: false },
550
						/* Filter tree         */ { sortable: false }
551
					],
552
					sorting: [[1, "asc"]],
553
					displayLength: 20,
554
					pagingType: "full_numbers"
555
			   });
556
557
				jQuery("#' . $table_id . '")
558
				/* Hide/show parents */
559
				.on("click", ".btn-toggle-parents", function() {
560
					jQuery(this).toggleClass("ui-state-active");
561
					jQuery(".parents", jQuery(this).closest("table").DataTable().rows().nodes()).slideToggle();
562
				})
563
				/* Hide/show statistics */
564
				.on("click",  ".btn-toggle-statistics", function() {
565
					jQuery(this).toggleClass("ui-state-active");
566
					jQuery("#fam_list_table-charts_' . $table_id . '").slideToggle();
567
				})
568
				/* Filter buttons in table header */
569
				.on("click", "button[data-filter-column]", function() {
570
					var btn = $(this);
571
					// De-activate the other buttons in this button group
572
					btn.siblings().removeClass("ui-state-active");
573
					// Apply (or clear) this filter
574
					var col = jQuery("#' . $table_id . '").DataTable().column(btn.data("filter-column"));
575
					if (btn.hasClass("ui-state-active")) {
576
						btn.removeClass("ui-state-active");
577
						col.search("").draw();
578
					} else {
579
						btn.addClass("ui-state-active");
580
						col.search(btn.data("filter-value")).draw();
581
					}
582
				});
583
584
				jQuery(".fam-list").css("visibility", "visible");
585
				jQuery(".loading-image").css("display", "none");
586
		');
587
588
        $max_age = (int) $WT_TREE->getPreference('MAX_ALIVE_AGE');
589
590
        // init chart data
591
        $marr_by_age = array();
592
        for ($age = 0; $age <= $max_age; $age++) {
593
            $marr_by_age[$age] = '';
594
        }
595
        $birt_by_decade = array();
596
        $marr_by_decade = array();
597
        for ($year = 1550; $year < 2030; $year += 10) {
598
            $birt_by_decade[$year] = '';
599
            $marr_by_decade[$year] = '';
600
        }
601
602
        $html = '
603
			<div class="loading-image"></div>
604
			<div class="fam-list">
605
				<table id="' . $table_id . '">
606
					<thead>
607
						<tr>
608
							<th colspan="14">
609
								<div class="btn-toolbar">
610
									<div class="btn-group">
611
										<button
612
											type="button"
613
											data-filter-column="12"
614
											data-filter-value="N"
615
											class="ui-state-default"
616
											title="' . I18N::translate('Show individuals who are alive or couples where both partners are alive.') . '"
617
										>
618
											' . I18N::translate('Both alive') . '
619
										</button>
620
										<button
621
											type="button"
622
											data-filter-column="12"
623
											data-filter-value="W"
624
											class="ui-state-default"
625
											title="' . I18N::translate('Show couples where only the female partner is dead.') . '"
626
										>
627
											' . I18N::translate('Widower') . '
628
										</button>
629
										<button
630
											type="button"
631
											data-filter-column="12"
632
											data-filter-value="H"
633
											class="ui-state-default"
634
											title="' . I18N::translate('Show couples where only the male partner is dead.') . '"
635
										>
636
											' . I18N::translate('Widow') . '
637
										</button>
638
										<button
639
											type="button"
640
											data-filter-column="12"
641
											data-filter-value="Y"
642
											class="ui-state-default"
643
											title="' . I18N::translate('Show individuals who are dead or couples where both partners are dead.') . '"
644
										>
645
											' . I18N::translate('Both dead') . '
646
										</button>
647
									</div>
648
									<div class="btn-group">
649
										<button
650
											type="button"
651
											data-filter-column="13"
652
											data-filter-value="R"
653
											class="ui-state-default"
654
											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.') . '"
655
										>
656
											' . I18N::translate('Roots') . '
657
										</button>
658
										<button
659
											type="button"
660
											data-filter-column="13"
661
											data-filter-value="L"
662
											class="ui-state-default"
663
											title="' . I18N::translate('Show “leaves” couples or individuals. These are individuals who are alive but have no children recorded in the database.') . '"
664
										>
665
											' . I18N::translate('Leaves') . '
666
										</button>
667
									</div>
668
									<div class="btn-group">
669
										<button
670
											type="button"
671
											data-filter-column="11"
672
											data-filter-value="U"
673
											class="ui-state-default"
674
											title="' . I18N::translate('Show couples with an unknown marriage date.') . '"
675
										>
676
											' . GedcomTag::getLabel('MARR') . '
677
										</button>
678
										<button
679
											type="button"
680
											data-filter-column="11"
681
											data-filter-value="YES"
682
											class="ui-state-default"
683
											title="' . I18N::translate('Show couples who married more than 100 years ago.') . '"
684
										>
685
											' . GedcomTag::getLabel('MARR') . '&gt;100
686
										</button>
687
										<button
688
											type="button"
689
											data-filter-column="11"
690
											data-filter-value="Y100"
691
											class="ui-state-default"
692
											title="' . I18N::translate('Show couples who married within the last 100 years.') . '"
693
										>
694
											' . GedcomTag::getLabel('MARR') . '&lt;=100
695
										</button>
696
										<button
697
											type="button"
698
											data-filter-column="11"
699
											data-filter-value="D"
700
											class="ui-state-default"
701
											title="' . I18N::translate('Show divorced couples.') . '"
702
										>
703
											' . GedcomTag::getLabel('DIV') . '
704
										</button>
705
										<button
706
											type="button"
707
											data-filter-column="11"
708
											data-filter-value="M"
709
											class="ui-state-default"
710
											title="' . I18N::translate('Show couples where either partner married more than once.') . '"
711
										>
712
											' . I18N::translate('Multiple marriages') . '
713
										</button>
714
									</div>
715
								</div>
716
							</th>
717
						</tr>
718
						<tr>
719
							<th>' . GedcomTag::getLabel('GIVN') . '</th>
720
							<th>' . GedcomTag::getLabel('SURN') . '</th>
721
							<th>' . GedcomTag::getLabel('AGE') . '</th>
722
							<th>' . GedcomTag::getLabel('GIVN') . '</th>
723
							<th>' . GedcomTag::getLabel('SURN') . '</th>
724
							<th>' . GedcomTag::getLabel('AGE') . '</th>
725
							<th>' . GedcomTag::getLabel('MARR') . '</th>
726
							<th><i class="icon-reminder" title="' . I18N::translate('Anniversary') . '"></i></th>
727
							<th>' . GedcomTag::getLabel('PLAC') . '</th>
728
							<th><i class="icon-children" title="' . I18N::translate('Children') . '"></i></th>
729
							<th>' . GedcomTag::getLabel('CHAN') . '</th>
730
							<th hidden></th>
731
							<th hidden></th>
732
							<th hidden></th>
733
						</tr>
734
					</thead>
735
					<tfoot>
736
						<tr>
737
							<th colspan="14">
738
								<div class="btn-toolbar">
739
									<div class="btn-group">
740
										<button type="button" class="ui-state-default btn-toggle-parents">
741
											' . I18N::translate('Show parents') . '
742
										</button>
743
										<button type="button" class="ui-state-default btn-toggle-statistics">
744
											' . I18N::translate('Show statistics charts') . '
745
										</button>
746
									</div>
747
								</div>
748
							</th>
749
						</tr>
750
					</tfoot>
751
					<tbody>';
752
753
        $hundred_years_ago = new Date(date('Y') - 100);
754
755
        foreach ($families as $family) {
756
            // Retrieve husband and wife
757
            $husb = $family->getHusband();
758
            if ($husb === null) {
759
                $husb = new Individual('H', '0 @H@ INDI', null, $family->getTree());
760
            }
761
            $wife = $family->getWife();
762
            if ($wife === null) {
763
                $wife = new Individual('W', '0 @W@ INDI', null, $family->getTree());
764
            }
765
            if (!$family->canShow()) {
766
                continue;
767
            }
768
            if ($family->isPendingAddtion()) {
769
                $class = ' class="new"';
770
            } elseif ($family->isPendingDeletion()) {
771
                $class = ' class="old"';
772
            } else {
773
                $class = '';
774
            }
775
            $html .= '<tr' . $class . '>';
776
            // Husband name(s)
777
            // Extract Given names and Surnames for sorting
778
            list($surn_givn, $givn_surn) = self::sortableNames($husb);
779
780
            $html .= '<td colspan="2" data-sort="' . Filter::escapeHtml($givn_surn) . '">';
781
            foreach ($husb->getAllNames() as $num => $name) {
782
                if ($name['type'] == 'NAME') {
783
                    $title = '';
784
                } else {
785
                    $title = 'title="' . strip_tags(GedcomTag::getLabel($name['type'], $husb)) . '"';
786
                }
787
                if ($num == $husb->getPrimaryName()) {
788
                    $class             = ' class="name2"';
789
                    $sex_image         = $husb->getSexImage();
790
                } else {
791
                    $class     = '';
792
                    $sex_image = '';
793
                }
794
                // Only show married names if they are the name we are filtering by.
795
                if ($name['type'] != '_MARNM' || $num == $husb->getPrimaryName()) {
796
                    $html .= '<a ' . $title . ' href="' . $family->getHtmlUrl() . '"' . $class . '>' . FunctionsPrint::highlightSearchHits($name['full']) . '</a>' . $sex_image . '<br>';
797
                }
798
            }
799
            // Husband parents
800
            $html .= $husb->getPrimaryParentsNames('parents details1', 'none');
801
            $html .= '</td>';
802
803
            // Hidden column for sortable name
804
            $html .= '<td hidden data-sort="' . Filter::escapeHtml($surn_givn) . '"></td>';
805
806
            // Husband age
807
            $mdate = $family->getMarriageDate();
808
            $hdate = $husb->getBirthDate();
809
            if ($hdate->isOK() && $mdate->isOK()) {
810
                if ($hdate->gregorianYear() >= 1550 && $hdate->gregorianYear() < 2030) {
811
                    $birt_by_decade[(int) ($hdate->gregorianYear() / 10) * 10] .= $husb->getSex();
812
                }
813
                $hage = Date::getAge($hdate, $mdate, 0);
814
                if ($hage >= 0 && $hage <= $max_age) {
815
                    $marr_by_age[$hage] .= $husb->getSex();
816
                }
817
            }
818
            $html .= '<td class="center" data=-sort="' . Date::getAge($hdate, $mdate, 1) . '">' . Date::getAge($hdate, $mdate, 2) . '</td>';
819
820
            // Wife name(s)
821
            // Extract Given names and Surnames for sorting
822
            list($surn_givn, $givn_surn) = self::sortableNames($wife);
823
            $html .= '<td colspan="2" data-sort="' . Filter::escapeHtml($givn_surn) . '">';
824
            foreach ($wife->getAllNames() as $num => $name) {
825
                if ($name['type'] == 'NAME') {
826
                    $title = '';
827
                } else {
828
                    $title = 'title="' . strip_tags(GedcomTag::getLabel($name['type'], $wife)) . '"';
829
                }
830
                if ($num == $wife->getPrimaryName()) {
831
                    $class             = ' class="name2"';
832
                    $sex_image         = $wife->getSexImage();
833
                } else {
834
                    $class     = '';
835
                    $sex_image = '';
836
                }
837
                // Only show married names if they are the name we are filtering by.
838
                if ($name['type'] != '_MARNM' || $num == $wife->getPrimaryName()) {
839
                    $html .= '<a ' . $title . ' href="' . $family->getHtmlUrl() . '"' . $class . '>' . FunctionsPrint::highlightSearchHits($name['full']) . '</a>' . $sex_image . '<br>';
840
                }
841
            }
842
            // Wife parents
843
            $html .= $wife->getPrimaryParentsNames('parents details1', 'none');
844
            $html .= '</td>';
845
846
            // Hidden column for sortable name
847
            $html .= '<td hidden data-sort="' . Filter::escapeHtml($surn_givn) . '"></td>';
848
849
            // Wife age
850
            $mdate = $family->getMarriageDate();
851
            $wdate = $wife->getBirthDate();
852
            if ($wdate->isOK() && $mdate->isOK()) {
853
                if ($wdate->gregorianYear() >= 1550 && $wdate->gregorianYear() < 2030) {
854
                    $birt_by_decade[(int) ($wdate->gregorianYear() / 10) * 10] .= $wife->getSex();
855
                }
856
                $wage = Date::getAge($wdate, $mdate, 0);
857
                if ($wage >= 0 && $wage <= $max_age) {
858
                    $marr_by_age[$wage] .= $wife->getSex();
859
                }
860
            }
861
            $html .= '<td class="center" data-sort="' . Date::getAge($wdate, $mdate, 1) . '">' . Date::getAge($wdate, $mdate, 2) . '</td>';
862
863
            // Marriage date
864
            $html .= '<td data-sort="' . $family->getMarriageDate()->julianDay() . '">';
865
            if ($marriage_dates = $family->getAllMarriageDates()) {
866
                foreach ($marriage_dates as $n => $marriage_date) {
867
                    if ($n) {
868
                        $html .= '<br>';
869
                    }
870
                    $html .= '<div>' . $marriage_date->display(true) . '</div>';
871
                }
872
                if ($marriage_dates[0]->gregorianYear() >= 1550 && $marriage_dates[0]->gregorianYear() < 2030) {
873
                    $marr_by_decade[(int) ($marriage_dates[0]->gregorianYear() / 10) * 10] .= $husb->getSex() . $wife->getSex();
874
                }
875
            } elseif ($family->getFacts('_NMR')) {
876
                $html .= I18N::translate('no');
877
            } elseif ($family->getFacts('MARR')) {
878
                $html .= I18N::translate('yes');
879
            } else {
880
                $html .= '&nbsp;';
881
            }
882
            $html .= '</td>';
883
884
            // Marriage anniversary
885
            $html .= '<td class="center" data-sort="' . -$family->getMarriageDate()->julianDay() . '">' . Date::getAge($family->getMarriageDate(), null, 2) . '</td>';
886
887
            // Marriage place
888
            $html .= '<td>';
889
            foreach ($family->getAllMarriagePlaces() as $n => $marriage_place) {
890
                $tmp = new Place($marriage_place, $family->getTree());
891
                if ($n) {
892
                    $html .= '<br>';
893
                }
894
                $html .= '<a href="' . $tmp->getURL() . '" title="' . strip_tags($tmp->getFullName()) . '">';
895
                $html .= FunctionsPrint::highlightSearchHits($tmp->getShortName()) . '</a>';
896
            }
897
            $html .= '</td>';
898
899
            // Number of children
900
            $html .= '<td class="center" data-sort="' . $family->getNumberOfChildren() . '">' . I18N::number($family->getNumberOfChildren()) . '</td>';
901
902
            // Last change
903
            $html .= '<td data-sort="' . $family->lastChangeTimestamp(true) . '">' . $family->lastChangeTimestamp() . '</td>';
904
905
            // Filter by marriage date
906
            $html .= '<td hidden>';
907
            if (!$family->canShow() || !$mdate->isOK()) {
908
                $html .= 'U';
909
            } else {
910
                if (Date::compare($mdate, $hundred_years_ago) > 0) {
911
                    $html .= 'Y100';
912
                } else {
913
                    $html .= 'YES';
914
                }
915
            }
916
            if ($family->getFacts(WT_EVENTS_DIV)) {
917
                $html .= 'D';
918
            }
919
            if (count($husb->getSpouseFamilies()) > 1 || count($wife->getSpouseFamilies()) > 1) {
920
                $html .= 'M';
921
            }
922
            $html .= '</td>';
923
924
            // Filter by alive/dead
925
            $html .= '<td hidden>';
926
            if ($husb->isDead() && $wife->isDead()) {
927
                $html .= 'Y';
928
            }
929
            if ($husb->isDead() && !$wife->isDead()) {
930
                if ($wife->getSex() == 'F') {
931
                    $html .= 'H';
932
                }
933
                if ($wife->getSex() == 'M') {
934
                    $html .= 'W';
935
                } // male partners
936
            }
937
            if (!$husb->isDead() && $wife->isDead()) {
938
                if ($husb->getSex() == 'M') {
939
                    $html .= 'W';
940
                }
941
                if ($husb->getSex() == 'F') {
942
                    $html .= 'H';
943
                } // female partners
944
            }
945
            if (!$husb->isDead() && !$wife->isDead()) {
946
                $html .= 'N';
947
            }
948
            $html .= '</td>';
949
950
            // Filter by roots/leaves
951
            $html .= '<td hidden>';
952
            if (!$husb->getChildFamilies() && !$wife->getChildFamilies()) {
953
                $html .= 'R';
954
            } elseif (!$husb->isDead() && !$wife->isDead() && $family->getNumberOfChildren() === 0) {
955
                $html .= 'L';
956
            }
957
            $html .= '</td>
958
			</tr>';
959
        }
960
961
        $html .= '
962
					</tbody>
963
				</table>
964
				<div id="fam_list_table-charts_' . $table_id . '" style="display:none">
965
					<table class="list-charts">
966
						<tr>
967
							<td>' . self::chartByDecade($birt_by_decade, I18N::translate('Decade of birth')) . '</td>
968
							<td>' . self::chartByDecade($marr_by_decade, I18N::translate('Decade of marriage')) . '</td>
969
						</tr>
970
						<tr>
971
							<td colspan="2">' . self::chartByAge($marr_by_age, I18N::translate('Age in year of marriage')) . '</td>
972
						</tr>
973
					</table>
974
				</div>
975
			</div>';
976
977
        return $html;
978
    }
979
980
    /**
981
     * Print a table of sources
982
     *
983
     * @param Source[] $sources
984
     *
985
     * @return string
986
     */
987
    public static function sourceTable($sources)
988
    {
989
        global $WT_TREE, $controller;
990
991
        // Count the number of linked records. These numbers include private records.
992
        // It is not good to bypass privacy, but many servers do not have the resources
993
        // to process privacy for every record in the tree
994
        $count_individuals = Database::prepare(
995
            "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"
996
        )->fetchAssoc();
997
        $count_families = Database::prepare(
998
            "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"
999
        )->fetchAssoc();
1000
        $count_media = Database::prepare(
1001
            "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"
1002
        )->fetchAssoc();
1003
        $count_notes = Database::prepare(
1004
            "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"
1005
        )->fetchAssoc();
1006
1007
        $html     = '';
1008
        $table_id = 'table-sour-' . Uuid::uuid4(); // lists requires a unique ID in case there are multiple lists per page
1009
        $controller
1010
            ->addExternalJavascript(WT_JQUERY_DATATABLES_JS_URL)
1011
            ->addInlineJavascript('
1012
				jQuery.fn.dataTableExt.oSort["text-asc"] = textCompareAsc;
1013
				jQuery.fn.dataTableExt.oSort["text-desc"] = textCompareDesc;
1014
				jQuery("#' . $table_id . '").dataTable( {
1015
					dom: \'<"H"pf<"dt-clear">irl>t<"F"pl>\',
1016
					' . I18N::datatablesI18N() . ',
1017
					jQueryUI: true,
1018
					autoWidth: false,
1019
					processing: true,
1020
					columns: [
1021
						/* Title         */ { type: "text" },
1022
						/* Author        */ { type: "text" },
1023
						/* Individuals   */ { type: "num" },
1024
						/* Families      */ { type: "num" },
1025
						/* Media objects */ { type: "num" },
1026
						/* Notes         */ { type: "num" },
1027
						/* Last change   */ { visible: ' . ($WT_TREE->getPreference('SHOW_LAST_CHANGE') ? 'true' : 'false') . ' },
1028
						/* Delete        */ { visible: ' . (Auth::isManager($WT_TREE) ? 'true' : 'false') . ', sortable: false }
1029
					],
1030
					displayLength: 20,
1031
					pagingType: "full_numbers"
1032
			   });
1033
				jQuery(".source-list").css("visibility", "visible");
1034
				jQuery(".loading-image").css("display", "none");
1035
			');
1036
1037
        $html .= '<div class="loading-image"></div>';
1038
        $html .= '<div class="source-list">';
1039
        $html .= '<table id="' . $table_id . '"><thead><tr>';
1040
        $html .= '<th>' . GedcomTag::getLabel('TITL') . '</th>';
1041
        $html .= '<th>' . GedcomTag::getLabel('AUTH') . '</th>';
1042
        $html .= '<th>' . I18N::translate('Individuals') . '</th>';
1043
        $html .= '<th>' . I18N::translate('Families') . '</th>';
1044
        $html .= '<th>' . I18N::translate('Media objects') . '</th>';
1045
        $html .= '<th>' . I18N::translate('Shared notes') . '</th>';
1046
        $html .= '<th>' . GedcomTag::getLabel('CHAN') . '</th>';
1047
        $html .= '<th>' . I18N::translate('Delete') . '</th>';
1048
        $html .= '</tr></thead>';
1049
        $html .= '<tbody>';
1050
1051
        foreach ($sources as $source) {
1052
            if (!$source->canShow()) {
1053
                continue;
1054
            }
1055
            if ($source->isPendingAddtion()) {
1056
                $class = ' class="new"';
1057
            } elseif ($source->isPendingDeletion()) {
1058
                $class = ' class="old"';
1059
            } else {
1060
                $class = '';
1061
            }
1062
            $html .= '<tr' . $class . '>';
1063
            // Source name(s)
1064
            $html .= '<td data-sort="' . Filter::escapeHtml($source->getSortName()) . '">';
1065
            foreach ($source->getAllNames() as $n => $name) {
1066
                if ($n) {
1067
                    $html .= '<br>';
1068
                }
1069
                if ($n == $source->getPrimaryName()) {
1070
                    $html .= '<a class="name2" href="' . $source->getHtmlUrl() . '">' . FunctionsPrint::highlightSearchHits($name['full']) . '</a>';
1071
                } else {
1072
                    $html .= '<a href="' . $source->getHtmlUrl() . '">' . FunctionsPrint::highlightSearchHits($name['full']) . '</a>';
1073
                }
1074
            }
1075
            $html .= '</td>';
1076
            // Author
1077
            $auth = $source->getFirstFact('AUTH');
1078
            if ($auth) {
1079
                $author = $auth->getValue();
1080
            } else {
1081
                $author = '';
1082
            }
1083
            $html .= '<td data-sort="' . Filter::escapeHtml($author) . '">' . FunctionsPrint::highlightSearchHits($author) . '</td>';
1084
            $key = $source->getXref() . '@' . $source->getTree()->getTreeId();
1085
            // Count of linked individuals
1086
            $num = array_key_exists($key, $count_individuals) ? $count_individuals[$key] : 0;
1087
            $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

1087
            $html .= '<td class="center" data-sort="' . $num . '">' . I18N::number(/** @scrutinizer ignore-type */ $num) . '</td>';
Loading history...
1088
            // Count of linked families
1089
            $num = array_key_exists($key, $count_families) ? $count_families[$key] : 0;
1090
            $html .= '<td class="center" data-sort="' . $num . '">' . I18N::number($num) . '</td>';
1091
            // Count of linked media objects
1092
            $num = array_key_exists($key, $count_media) ? $count_media[$key] : 0;
1093
            $html .= '<td class="center" data-sort="' . $num . '">' . I18N::number($num) . '</td>';
1094
            // Count of linked notes
1095
            $num = array_key_exists($key, $count_notes) ? $count_notes[$key] : 0;
1096
            $html .= '<td class="center" data-sort="' . $num . '">' . I18N::number($num) . '</td>';
1097
            // Last change
1098
            $html .= '<td data-sort="' . $source->lastChangeTimestamp(true) . '">' . $source->lastChangeTimestamp() . '</td>';
1099
            // Delete
1100
            $html .= '<td><a href="#" title="' . I18N::translate('Delete') . '" class="deleteicon" onclick="return delete_record(\'' . I18N::translate('Are you sure you want to delete “%s”?', Filter::escapeJs(Filter::unescapeHtml($source->getFullName()))) . "', '" . $source->getXref() . '\');"><span class="link_text">' . I18N::translate('Delete') . '</span></a></td>';
1101
            $html .= '</tr>';
1102
        }
1103
        $html .= '</tbody></table></div>';
1104
1105
        return $html;
1106
    }
1107
1108
    /**
1109
     * Print a table of shared notes
1110
     *
1111
     * @param Note[] $notes
1112
     *
1113
     * @return string
1114
     */
1115
    public static function noteTable($notes)
1116
    {
1117
        global $WT_TREE, $controller;
1118
1119
        // Count the number of linked records. These numbers include private records.
1120
        // It is not good to bypass privacy, but many servers do not have the resources
1121
        // to process privacy for every record in the tree
1122
        $count_individuals = Database::prepare(
1123
            "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"
1124
        )->fetchAssoc();
1125
        $count_families = Database::prepare(
1126
            "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"
1127
        )->fetchAssoc();
1128
        $count_media = Database::prepare(
1129
            "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"
1130
        )->fetchAssoc();
1131
        $count_sources = Database::prepare(
1132
            "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"
1133
        )->fetchAssoc();
1134
1135
        $html     = '';
1136
        $table_id = 'table-note-' . Uuid::uuid4(); // lists requires a unique ID in case there are multiple lists per page
1137
        $controller
1138
            ->addExternalJavascript(WT_JQUERY_DATATABLES_JS_URL)
1139
            ->addInlineJavascript('
1140
				jQuery.fn.dataTableExt.oSort["text-asc"] = textCompareAsc;
1141
				jQuery.fn.dataTableExt.oSort["text-desc"] = textCompareDesc;
1142
				jQuery("#' . $table_id . '").dataTable({
1143
					dom: \'<"H"pf<"dt-clear">irl>t<"F"pl>\',
1144
					' . I18N::datatablesI18N() . ',
1145
					jQueryUI: true,
1146
					autoWidth: false,
1147
					processing: true,
1148
					columns: [
1149
						/* Title         */ { type: "text" },
1150
						/* Individuals   */ { type: "num" },
1151
						/* Families      */ { type: "num" },
1152
						/* Media objects */ { type: "num" },
1153
						/* Sources       */ { type: "num" },
1154
						/* Last change   */ { type: "num", visible: ' . ($WT_TREE->getPreference('SHOW_LAST_CHANGE') ? 'true' : 'false') . ' },
1155
						/* Delete        */ { visible: ' . (Auth::isManager($WT_TREE) ? 'true' : 'false') . ', sortable: false }
1156
					],
1157
					displayLength: 20,
1158
					pagingType: "full_numbers"
1159
				});
1160
				jQuery(".note-list").css("visibility", "visible");
1161
				jQuery(".loading-image").css("display", "none");
1162
			');
1163
1164
        $html .= '<div class="loading-image"></div>';
1165
        $html .= '<div class="note-list">';
1166
        $html .= '<table id="' . $table_id . '"><thead><tr>';
1167
        $html .= '<th>' . GedcomTag::getLabel('TITL') . '</th>';
1168
        $html .= '<th>' . I18N::translate('Individuals') . '</th>';
1169
        $html .= '<th>' . I18N::translate('Families') . '</th>';
1170
        $html .= '<th>' . I18N::translate('Media objects') . '</th>';
1171
        $html .= '<th>' . I18N::translate('Sources') . '</th>';
1172
        $html .= '<th>' . GedcomTag::getLabel('CHAN') . '</th>';
1173
        $html .= '<th>' . I18N::translate('Delete') . '</th>';
1174
        $html .= '</tr></thead>';
1175
        $html .= '<tbody>';
1176
1177
        foreach ($notes as $note) {
1178
            if (!$note->canShow()) {
1179
                continue;
1180
            }
1181
            if ($note->isPendingAddtion()) {
1182
                $class = ' class="new"';
1183
            } elseif ($note->isPendingDeletion()) {
1184
                $class = ' class="old"';
1185
            } else {
1186
                $class = '';
1187
            }
1188
            $html .= '<tr' . $class . '>';
1189
            // Count of linked notes
1190
            $html .= '<td data-sort="' . Filter::escapeHtml($note->getSortName()) . '"><a class="name2" href="' . $note->getHtmlUrl() . '">' . FunctionsPrint::highlightSearchHits($note->getFullName()) . '</a></td>';
1191
            $key = $note->getXref() . '@' . $note->getTree()->getTreeId();
1192
            // Count of linked individuals
1193
            $num = array_key_exists($key, $count_individuals) ? $count_individuals[$key] : 0;
1194
            $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

1194
            $html .= '<td class="center" data-sort="' . $num . '">' . I18N::number(/** @scrutinizer ignore-type */ $num) . '</td>';
Loading history...
1195
            // Count of linked families
1196
            $num = array_key_exists($key, $count_families) ? $count_families[$key] : 0;
1197
            $html .= '<td class="center" data-sort="' . $num . '">' . I18N::number($num) . '</td>';
1198
            // Count of linked media objects
1199
            $num = array_key_exists($key, $count_media) ? $count_media[$key] : 0;
1200
            $html .= '<td class="center" data-sort="' . $num . '">' . I18N::number($num) . '</td>';
1201
            // Count of linked sources
1202
            $num = array_key_exists($key, $count_sources) ? $count_sources[$key] : 0;
1203
            $html .= '<td class="center" data-sort="' . $num . '">' . I18N::number($num) . '</td>';
1204
            // Last change
1205
            $html .= '<td data-sort="' . $note->lastChangeTimestamp(true) . '">' . $note->lastChangeTimestamp() . '</td>';
1206
            // Delete
1207
            $html .= '<td><a href="#" title="' . I18N::translate('Delete') . '" class="deleteicon" onclick="return delete_record(\'' . I18N::translate('Are you sure you want to delete “%s”?', Filter::escapeJs(Filter::unescapeHtml($note->getFullName()))) . "', '" . $note->getXref() . '\');"><span class="link_text">' . I18N::translate('Delete') . '</span></a></td>';
1208
            $html .= '</tr>';
1209
        }
1210
        $html .= '</tbody></table></div>';
1211
1212
        return $html;
1213
    }
1214
1215
    /**
1216
     * Print a table of repositories
1217
     *
1218
     * @param Repository[] $repositories
1219
     *
1220
     * @return string
1221
     */
1222
    public static function repositoryTable($repositories)
1223
    {
1224
        global $WT_TREE, $controller;
1225
1226
        // Count the number of linked records. These numbers include private records.
1227
        // It is not good to bypass privacy, but many servers do not have the resources
1228
        // to process privacy for every record in the tree
1229
        $count_sources = Database::prepare(
1230
            "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"
1231
        )->fetchAssoc();
1232
1233
        $html     = '';
1234
        $table_id = 'table-repo-' . Uuid::uuid4(); // lists requires a unique ID in case there are multiple lists per page
1235
        $controller
1236
            ->addExternalJavascript(WT_JQUERY_DATATABLES_JS_URL)
1237
            ->addInlineJavascript('
1238
				jQuery.fn.dataTableExt.oSort["text-asc"] = textCompareAsc;
1239
				jQuery.fn.dataTableExt.oSort["text-desc"] = textCompareDesc;
1240
				jQuery("#' . $table_id . '").dataTable({
1241
					dom: \'<"H"pf<"dt-clear">irl>t<"F"pl>\',
1242
					' . I18N::datatablesI18N() . ',
1243
					jQueryUI: true,
1244
					autoWidth: false,
1245
					processing: true,
1246
					columns: [
1247
						/* Name        */ { type: "text" },
1248
						/* Sources     */ { type: "num" },
1249
						/* Last change */ { visible: ' . ($WT_TREE->getPreference('SHOW_LAST_CHANGE') ? 'true' : 'false') . ' },
1250
						/* Delete      */ { visible: ' . (Auth::isManager($WT_TREE) ? 'true' : 'false') . ', sortable: false }
1251
					],
1252
					displayLength: 20,
1253
					pagingType: "full_numbers"
1254
				});
1255
				jQuery(".repo-list").css("visibility", "visible");
1256
				jQuery(".loading-image").css("display", "none");
1257
			');
1258
1259
        $html .= '<div class="loading-image"></div>';
1260
        $html .= '<div class="repo-list">';
1261
        $html .= '<table id="' . $table_id . '"><thead><tr>';
1262
        $html .= '<th>' . I18N::translate('Repository name') . '</th>';
1263
        $html .= '<th>' . I18N::translate('Sources') . '</th>';
1264
        $html .= '<th>' . GedcomTag::getLabel('CHAN') . '</th>';
1265
        $html .= '<th>' . I18N::translate('Delete') . '</th>';
1266
        $html .= '</tr></thead>';
1267
        $html .= '<tbody>';
1268
1269
        foreach ($repositories as $repository) {
1270
            if (!$repository->canShow()) {
1271
                continue;
1272
            }
1273
            if ($repository->isPendingAddtion()) {
1274
                $class = ' class="new"';
1275
            } elseif ($repository->isPendingDeletion()) {
1276
                $class = ' class="old"';
1277
            } else {
1278
                $class = '';
1279
            }
1280
            $html .= '<tr' . $class . '>';
1281
            // Repository name(s)
1282
            $html .= '<td data-sort="' . Filter::escapeHtml($repository->getSortName()) . '">';
1283
            foreach ($repository->getAllNames() as $n => $name) {
1284
                if ($n) {
1285
                    $html .= '<br>';
1286
                }
1287
                if ($n == $repository->getPrimaryName()) {
1288
                    $html .= '<a class="name2" href="' . $repository->getHtmlUrl() . '">' . FunctionsPrint::highlightSearchHits($name['full']) . '</a>';
1289
                } else {
1290
                    $html .= '<a href="' . $repository->getHtmlUrl() . '">' . FunctionsPrint::highlightSearchHits($name['full']) . '</a>';
1291
                }
1292
            }
1293
            $html .= '</td>';
1294
            $key = $repository->getXref() . '@' . $repository->getTree()->getTreeId();
1295
            // Count of linked sources
1296
            $num = array_key_exists($key, $count_sources) ? $count_sources[$key] : 0;
1297
            $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

1297
            $html .= '<td class="center" data-sort="' . $num . '">' . I18N::number(/** @scrutinizer ignore-type */ $num) . '</td>';
Loading history...
1298
            // Last change
1299
            $html .= '<td data-sort="' . $repository->lastChangeTimestamp(true) . '">' . $repository->lastChangeTimestamp() . '</td>';
1300
            // Delete
1301
            $html .= '<td><a href="#" title="' . I18N::translate('Delete') . '" class="deleteicon" onclick="return delete_record(\'' . I18N::translate('Are you sure you want to delete “%s”?', Filter::escapeJs(Filter::unescapeHtml($repository->getFullName()))) . "', '" . $repository->getXref() . '\');"><span class="link_text">' . I18N::translate('Delete') . '</span></a></td>';
1302
            $html .= '</tr>';
1303
        }
1304
        $html .= '</tbody></table></div>';
1305
1306
        return $html;
1307
    }
1308
1309
    /**
1310
     * Print a table of media objects
1311
     *
1312
     * @param Media[] $media_objects
1313
     *
1314
     * @return string
1315
     */
1316
    public static function mediaTable($media_objects)
1317
    {
1318
        global $WT_TREE, $controller;
1319
1320
        $html     = '';
1321
        $table_id = 'table-obje-' . Uuid::uuid4(); // lists requires a unique ID in case there are multiple lists per page
1322
        $controller
1323
            ->addExternalJavascript(WT_JQUERY_DATATABLES_JS_URL)
1324
            ->addInlineJavascript('
1325
				jQuery.fn.dataTableExt.oSort["text-asc"] = textCompareAsc;
1326
				jQuery.fn.dataTableExt.oSort["text-desc"] = textCompareDesc;
1327
				jQuery("#' . $table_id . '").dataTable({
1328
					dom: \'<"H"pf<"dt-clear">irl>t<"F"pl>\',
1329
					' . I18N::datatablesI18N() . ',
1330
					jQueryUI: true,
1331
					autoWidth:false,
1332
					processing: true,
1333
					columns: [
1334
						/* Thumbnail   */ { sortable: false },
1335
						/* Title       */ { type: "text" },
1336
						/* Individuals */ { type: "num" },
1337
						/* Families    */ { type: "num" },
1338
						/* Sources     */ { type: "num" },
1339
						/* Last change */ { visible: ' . ($WT_TREE->getPreference('SHOW_LAST_CHANGE') ? 'true' : 'false') . ' },
1340
					],
1341
					displayLength: 20,
1342
					pagingType: "full_numbers"
1343
				});
1344
				jQuery(".media-list").css("visibility", "visible");
1345
				jQuery(".loading-image").css("display", "none");
1346
			');
1347
1348
        $html .= '<div class="loading-image"></div>';
1349
        $html .= '<div class="media-list">';
1350
        $html .= '<table id="' . $table_id . '"><thead><tr>';
1351
        $html .= '<th>' . I18N::translate('Media') . '</th>';
1352
        $html .= '<th>' . GedcomTag::getLabel('TITL') . '</th>';
1353
        $html .= '<th>' . I18N::translate('Individuals') . '</th>';
1354
        $html .= '<th>' . I18N::translate('Families') . '</th>';
1355
        $html .= '<th>' . I18N::translate('Sources') . '</th>';
1356
        $html .= '<th>' . GedcomTag::getLabel('CHAN') . '</th>';
1357
        $html .= '</tr></thead>';
1358
        $html .= '<tbody>';
1359
1360
        foreach ($media_objects as $media_object) {
1361
            if ($media_object->canShow()) {
1362
                $name = $media_object->getFullName();
1363
                if ($media_object->isPendingAddtion()) {
1364
                    $class = ' class="new"';
1365
                } elseif ($media_object->isPendingDeletion()) {
1366
                    $class = ' class="old"';
1367
                } else {
1368
                    $class = '';
1369
                }
1370
                $html .= '<tr' . $class . '>';
1371
                // Media object thumbnail
1372
                $html .= '<td>' . $media_object->displayImage() . '</td>';
1373
                // Media object name(s)
1374
                $html .= '<td data-sort="' . Filter::escapeHtml($media_object->getSortName()) . '">';
1375
                $html .= '<a href="' . $media_object->getHtmlUrl() . '" class="list_item name2">';
1376
                $html .= FunctionsPrint::highlightSearchHits($name) . '</a>';
1377
                if (Auth::isEditor($media_object->getTree())) {
1378
                    $html .= '<br><a href="' . $media_object->getHtmlUrl() . '">' . basename($media_object->getFilename()) . '</a>';
1379
                }
1380
                $html .= '</td>';
1381
1382
                // Count of linked individuals
1383
                $num = count($media_object->linkedIndividuals('OBJE'));
1384
                $html .= '<td class="center" data-sort="' . $num . '">' . I18N::number($num) . '</td>';
1385
                // Count of linked families
1386
                $num = count($media_object->linkedFamilies('OBJE'));
1387
                $html .= '<td class="center" data-sort="' . $num . '">' . I18N::number($num) . '</td>';
1388
                // Count of linked sources
1389
                $num = count($media_object->linkedSources('OBJE'));
1390
                $html .= '<td class="center" data-sort="' . $num . '">' . I18N::number($num) . '</td>';
1391
                // Last change
1392
                $html .= '<td data-sort="' . $media_object->lastChangeTimestamp(true) . '">' . $media_object->lastChangeTimestamp() . '</td>';
1393
                $html .= '</tr>';
1394
            }
1395
        }
1396
        $html .= '</tbody></table></div>';
1397
1398
        return $html;
1399
    }
1400
1401
    /**
1402
     * Print a table of surnames, for the top surnames block, the indi/fam lists, etc.
1403
     *
1404
     * @param string[][] $surnames array (of SURN, of array of SPFX_SURN, of array of PID)
1405
     * @param string $script "indilist.php" (counts of individuals) or "famlist.php" (counts of spouses)
1406
     * @param Tree $tree generate links for this tree
1407
     *
1408
     * @return string
1409
     */
1410
    public static function surnameTable($surnames, $script, Tree $tree)
1411
    {
1412
        global $controller;
1413
1414
        $html = '';
1415
        $controller
1416
            ->addExternalJavascript(WT_JQUERY_DATATABLES_JS_URL)
1417
            ->addInlineJavascript('
1418
				jQuery.fn.dataTableExt.oSort["text-asc"] = textCompareAsc;
1419
				jQuery.fn.dataTableExt.oSort["text-desc"] = textCompareDesc;
1420
				jQuery(".surname-list").dataTable({
1421
					dom: "t",
1422
					jQueryUI: true,
1423
					autoWidth: false,
1424
					' . I18N::datatablesI18N() . ',
1425
					paging: false,
1426
					sorting: [[0, "asc"]],
1427
					columns: [
1428
						/* Surname */ { type: "text" },
1429
						/* Count   */ { type: "num" }
1430
					]
1431
				});
1432
			');
1433
1434
        if ($script == 'famlist.php') {
1435
            $col_heading = I18N::translate('Spouses');
1436
        } else {
1437
            $col_heading = I18N::translate('Individuals');
1438
        }
1439
1440
        $html .=
1441
            '<table class="surname-list">' .
1442
            '<thead>' .
1443
            '<tr>' .
1444
            '<th>' . GedcomTag::getLabel('SURN') . '</th>' .
1445
            '<th>' . $col_heading . '</th>' .
1446
            '</tr>' .
1447
            '</thead>';
1448
1449
        $html .= '<tbody>';
1450
        foreach ($surnames as $surn => $surns) {
1451
            // Each surname links back to the indi/fam surname list
1452
            if ($surn) {
1453
                $url = $script . '?surname=' . rawurlencode($surn) . '&amp;ged=' . $tree->getNameUrl();
1454
            } else {
1455
                $url = $script . '?alpha=,&amp;ged=' . $tree->getNameUrl();
1456
            }
1457
            $html .= '<tr>';
1458
            // Surname
1459
            $html .= '<td data-sort="' . Filter::escapeHtml($surn) . '">';
1460
            // Multiple surname variants, e.g. von Groot, van Groot, van der Groot, etc.
1461
            foreach ($surns as $spfxsurn => $indis) {
1462
                if ($spfxsurn) {
1463
                    $html .= '<a href="' . $url . '" dir="auto">' . Filter::escapeHtml($spfxsurn) . '</a><br>';
1464
                } else {
1465
                    // No surname, but a value from "2 SURN"? A common workaround for toponyms, etc.
1466
                    $html .= '<a href="' . $url . '" dir="auto">' . Filter::escapeHtml($surn) . '</a><br>';
1467
                }
1468
            }
1469
            $html .= '</td>';
1470
            // Surname count
1471
            $subtotal = 0;
1472
            foreach ($surns as $indis) {
1473
                $subtotal += count($indis);
0 ignored issues
show
Bug introduced by
$indis of type string is incompatible with the type Countable|array expected by parameter $value 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

1473
                $subtotal += count(/** @scrutinizer ignore-type */ $indis);
Loading history...
1474
            }
1475
            $html .= '<td class="center" data-sort="' . $subtotal . '">';
1476
            foreach ($surns as $indis) {
1477
                $html .= I18N::number(count($indis)) . '<br>';
1478
            }
1479
            if (count($surns) > 1) {
1480
                // More than one surname variant? Show a subtotal
1481
                $html .= I18N::number($subtotal);
1482
            }
1483
            $html .= '</td>';
1484
            $html .= '</tr>';
1485
        }
1486
        $html .= '</tbody></table>';
1487
1488
        return $html;
1489
    }
1490
1491
    /**
1492
     * Print a tagcloud of surnames.
1493
     *
1494
     * @param string[][] $surnames array (of SURN, of array of SPFX_SURN, of array of PID)
1495
     * @param string $script indilist or famlist
1496
     * @param bool $totals show totals after each name
1497
     * @param Tree $tree generate links to this tree
1498
     *
1499
     * @return string
1500
     */
1501
    public static function surnameTagCloud($surnames, $script, $totals, Tree $tree)
1502
    {
1503
        $minimum = PHP_INT_MAX;
1504
        $maximum = 1;
1505
        foreach ($surnames as $surn => $surns) {
1506
            foreach ($surns as $spfxsurn => $indis) {
1507
                $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 $value 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

1507
                $maximum = max($maximum, count(/** @scrutinizer ignore-type */ $indis));
Loading history...
1508
                $minimum = min($minimum, count($indis));
1509
            }
1510
        }
1511
1512
        $html = '';
1513
        foreach ($surnames as $surn => $surns) {
1514
            foreach ($surns as $spfxsurn => $indis) {
1515
                if ($maximum === $minimum) {
1516
                    // All surnames occur the same number of times
1517
                    $size = 150.0;
1518
                } else {
1519
                    $size = 75.0 + 125.0 * (count($indis) - $minimum) / ($maximum - $minimum);
1520
                }
1521
                $html .= '<a style="font-size:' . $size . '%" href="' . $script . '?surname=' . Filter::escapeUrl($surn) . '&amp;ged=' . $tree->getNameUrl() . '">';
1522
                if ($totals) {
1523
                    $html .= I18N::translate('%1$s (%2$s)', '<span dir="auto">' . $spfxsurn . '</span>', I18N::number(count($indis)));
1524
                } else {
1525
                    $html .= $spfxsurn;
1526
                }
1527
                $html .= '</a> ';
1528
            }
1529
        }
1530
1531
        return '<div class="tag_cloud">' . $html . '</div>';
1532
    }
1533
1534
    /**
1535
     * Print a list of surnames.
1536
     *
1537
     * @param string[][] $surnames array (of SURN, of array of SPFX_SURN, of array of PID)
1538
     * @param int $style 1=bullet list, 2=semicolon-separated list, 3=tabulated list with up to 4 columns
1539
     * @param bool $totals show totals after each name
1540
     * @param string $script indilist or famlist
1541
     * @param Tree $tree Link back to the individual list in this tree
1542
     *
1543
     * @return string
1544
     */
1545
    public static function surnameList($surnames, $style, $totals, $script, Tree $tree)
1546
    {
1547
        $html = array();
1548
        foreach ($surnames as $surn => $surns) {
1549
            // Each surname links back to the indilist
1550
            if ($surn) {
1551
                $url = $script . '?surname=' . urlencode($surn) . '&amp;ged=' . $tree->getNameUrl();
1552
            } else {
1553
                $url = $script . '?alpha=,&amp;ged=' . $tree->getNameUrl();
1554
            }
1555
            // If all the surnames are just case variants, then merge them into one
1556
            // Comment out this block if you want SMITH listed separately from Smith
1557
            $first_spfxsurn = null;
1558
            foreach ($surns as $spfxsurn => $indis) {
1559
                if ($first_spfxsurn) {
1560
                    if (I18N::strtoupper($spfxsurn) == I18N::strtoupper($first_spfxsurn)) {
0 ignored issues
show
Bug introduced by
$first_spfxsurn of type void is incompatible with the type string expected by parameter $string of Fisharebest\Webtrees\I18N::strtoupper(). ( Ignorable by Annotation )

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

1560
                    if (I18N::strtoupper($spfxsurn) == I18N::strtoupper(/** @scrutinizer ignore-type */ $first_spfxsurn)) {
Loading history...
1561
                        $surns[$first_spfxsurn] = array_merge($surns[$first_spfxsurn], $surns[$spfxsurn]);
0 ignored issues
show
Bug introduced by
$surns[$first_spfxsurn] of type string is incompatible with the type array expected by parameter $arrays of array_merge(). ( Ignorable by Annotation )

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

1561
                        $surns[$first_spfxsurn] = array_merge(/** @scrutinizer ignore-type */ $surns[$first_spfxsurn], $surns[$spfxsurn]);
Loading history...
1562
                        unset($surns[$spfxsurn]);
1563
                    }
1564
                } else {
1565
                    $first_spfxsurn = $spfxsurn;
1566
                }
1567
            }
1568
            $subhtml = '<a href="' . $url . '" dir="auto">' . Filter::escapeHtml(implode(I18N::$list_separator, array_keys($surns))) . '</a>';
1569
1570
            if ($totals) {
1571
                $subtotal = 0;
1572
                foreach ($surns as $indis) {
1573
                    $subtotal += count($indis);
1574
                }
1575
                $subhtml .= '&nbsp;(' . I18N::number($subtotal) . ')';
1576
            }
1577
            $html[] = $subhtml;
1578
        }
1579
        switch ($style) {
1580
            case 1:
1581
                return '<ul><li>' . implode('</li><li>', $html) . '</li></ul>';
1582
            case 2:
1583
                return implode(I18N::$list_separator, $html);
1584
            case 3:
1585
                $i     = 0;
1586
                $count = count($html);
1587
                if ($count > 36) {
1588
                    $col = 4;
1589
                } elseif ($count > 18) {
1590
                    $col = 3;
1591
                } elseif ($count > 6) {
1592
                    $col = 2;
1593
                } else {
1594
                    $col = 1;
1595
                }
1596
                $newcol = ceil($count / $col);
1597
                $html2  = '<table class="list_table"><tr>';
1598
                $html2 .= '<td class="list_value" style="padding: 14px;">';
1599
1600
                foreach ($html as $surns) {
1601
                    $html2 .= $surns . '<br>';
1602
                    $i++;
1603
                    if ($i == $newcol && $i < $count) {
1604
                        $html2 .= '</td><td class="list_value" style="padding: 14px;">';
1605
                        $newcol = $i + ceil($count / $col);
1606
                    }
1607
                }
1608
                $html2 .= '</td></tr></table>';
1609
1610
                return $html2;
1611
        }
1612
    }
1613
    /**
1614
     * Print a table of events
1615
     *
1616
     * @param int $startjd
1617
     * @param int $endjd
1618
     * @param string $events
1619
     * @param bool $only_living
1620
     * @param string $sort_by
1621
     *
1622
     * @return string
1623
     */
1624
    public static function eventsTable($startjd, $endjd, $events = 'BIRT MARR DEAT', $only_living = false, $sort_by = 'anniv')
1625
    {
1626
        global $controller, $WT_TREE;
1627
1628
        $html     = '';
1629
        $table_id = 'table-even-' . Uuid::uuid4(); // lists requires a unique ID in case there are multiple lists per page
1630
        $controller
1631
            ->addExternalJavascript(WT_JQUERY_DATATABLES_JS_URL)
1632
            ->addInlineJavascript('
1633
				jQuery.fn.dataTableExt.oSort["text-asc"] = textCompareAsc;
1634
				jQuery.fn.dataTableExt.oSort["text-desc"] = textCompareDesc;
1635
				jQuery("#' . $table_id . '").dataTable({
1636
					dom: "t",
1637
					' . I18N::datatablesI18N() . ',
1638
					autoWidth: false,
1639
					paging: false,
1640
					lengthChange: false,
1641
					filter: false,
1642
					info: true,
1643
					jQueryUI: true,
1644
					sorting: [[ ' . ($sort_by == 'alpha' ? 0 : 1) . ', "asc"]],
1645
					columns: [
1646
						/* Name        */ { type: "text" },
1647
						/* Date        */ { type: "num" },
1648
						/* Anniversary */ { type: "num" },
1649
						/* Event       */ { type: "text" }
1650
					]
1651
				});
1652
			');
1653
1654
        // Did we have any output? Did we skip anything?
1655
        $filter          = 0;
1656
        $filtered_events = array();
1657
1658
        foreach (FunctionsDb::getEventsList($startjd, $endjd, $events, $WT_TREE) as $fact) {
1659
            $record = $fact->getParent();
1660
            // Only living people ?
1661
            if ($only_living) {
1662
                if ($record instanceof Individual && $record->isDead()) {
1663
                    $filter++;
1664
                    continue;
1665
                }
1666
                if ($record instanceof Family) {
1667
                    $husb = $record->getHusband();
1668
                    if ($husb === null || $husb->isDead()) {
1669
                        $filter++;
1670
                        continue;
1671
                    }
1672
                    $wife = $record->getWife();
1673
                    if ($wife === null || $wife->isDead()) {
1674
                        $filter++;
1675
                        continue;
1676
                    }
1677
                }
1678
            }
1679
1680
            $filtered_events[] = $fact;
1681
        }
1682
1683
        if (!empty($filtered_events)) {
1684
            $html .= '<table id="' . $table_id . '" class="width100">';
1685
            $html .= '<thead><tr>';
1686
            $html .= '<th>' . I18N::translate('Record') . '</th>';
1687
            $html .= '<th>' . GedcomTag::getLabel('DATE') . '</th>';
1688
            $html .= '<th><i class="icon-reminder" title="' . I18N::translate('Anniversary') . '"></i></th>';
1689
            $html .= '<th>' . GedcomTag::getLabel('EVEN') . '</th>';
1690
            $html .= '</tr></thead><tbody>';
1691
1692
            foreach ($filtered_events as $n => $fact) {
1693
                $record = $fact->getParent();
1694
                $html .= '<tr>';
1695
                $html .= '<td data-sort="' . Filter::escapeHtml($record->getSortName()) . '">';
1696
                $html .= '<a href="' . $record->getHtmlUrl() . '">' . $record->getFullName() . '</a>';
1697
                if ($record instanceof Individual) {
1698
                    $html .= $record->getSexImage();
1699
                }
1700
                $html .= '</td>';
1701
                $html .= '<td data-sort="' . $fact->getDate()->minimumJulianDay() . '">';
1702
                $html .= $fact->getDate()->display();
1703
                $html .= '</td>';
1704
                $html .= '<td class="center" data-sort="' . $fact->anniv . '">';
1705
                $html .= ($fact->anniv ? I18N::number($fact->anniv) : '');
1706
                $html .= '</td>';
1707
                $html .= '<td class="center">' . $fact->getLabel() . '</td>';
1708
                $html .= '</tr>';
1709
            }
1710
1711
            $html .= '</tbody></table>';
1712
        } else {
1713
            if ($endjd === WT_CLIENT_JD) {
1714
                // We're dealing with the Today’s Events block
1715
                if ($filter === 0) {
1716
                    $html .=  I18N::translate('No events exist for today.');
1717
                } else {
1718
                    $html .=  I18N::translate('No events for living individuals exist for today.');
1719
                }
1720
            } else {
1721
                // We're dealing with the Upcoming Events block
1722
                if ($filter === 0) {
1723
                    if ($endjd === $startjd) {
1724
                        $html .=  I18N::translate('No events exist for tomorrow.');
1725
                    } else {
1726
                        $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));
1727
                    }
1728
                } else {
1729
                    if ($endjd === $startjd) {
1730
                        $html .=  I18N::translate('No events for living individuals exist for tomorrow.');
1731
                    } else {
1732
                        // I18N: translation for %s==1 is unused; it is translated separately as “tomorrow”
1733
                        $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));
1734
                    }
1735
                }
1736
            }
1737
        }
1738
1739
        return $html;
1740
    }
1741
1742
    /**
1743
     * Print a list of events
1744
     *
1745
     * This performs the same function as print_events_table(), but formats the output differently.
1746
     *
1747
     * @param int $startjd
1748
     * @param int $endjd
1749
     * @param string $events
1750
     * @param bool $only_living
1751
     * @param string $sort_by
1752
     *
1753
     * @return string
1754
     */
1755
    public static function eventsList($startjd, $endjd, $events = 'BIRT MARR DEAT', $only_living = false, $sort_by = 'anniv')
1756
    {
1757
        global $WT_TREE;
1758
1759
        // Did we have any output? Did we skip anything?
1760
        $output          = 0;
1761
        $filter          = 0;
1762
        $filtered_events = array();
1763
        $html            = '';
1764
        foreach (FunctionsDb::getEventsList($startjd, $endjd, $events, $WT_TREE) as $fact) {
1765
            $record = $fact->getParent();
1766
            // only living people ?
1767
            if ($only_living) {
1768
                if ($record instanceof Individual && $record->isDead()) {
1769
                    $filter++;
1770
                    continue;
1771
                }
1772
                if ($record instanceof Family) {
1773
                    $husb = $record->getHusband();
1774
                    if ($husb === null || $husb->isDead()) {
1775
                        $filter++;
1776
                        continue;
1777
                    }
1778
                    $wife = $record->getWife();
1779
                    if ($wife === null || $wife->isDead()) {
1780
                        $filter++;
1781
                        continue;
1782
                    }
1783
                }
1784
            }
1785
1786
            $output++;
1787
1788
            $filtered_events[] = $fact;
1789
        }
1790
1791
        // Now we've filtered the list, we can sort by event, if required
1792
        switch ($sort_by) {
1793
            case 'anniv':
1794
                // Data is already sorted by anniversary date
1795
                break;
1796
            case 'alpha':
1797
                uasort($filtered_events, function (Fact $x, Fact $y) {
1798
                    return GedcomRecord::compare($x->getParent(), $y->getParent());
1799
                });
1800
                break;
1801
        }
1802
1803
        foreach ($filtered_events as $fact) {
1804
            $record = $fact->getParent();
1805
            $html .= '<a href="' . $record->getHtmlUrl() . '" class="list_item name2">' . $record->getFullName() . '</a>';
1806
            if ($record instanceof Individual) {
1807
                $html .= $record->getSexImage();
1808
            }
1809
            $html .= '<br><div class="indent">';
1810
            $html .= $fact->getLabel() . ' — ' . $fact->getDate()->display(true);
1811
            if ($fact->anniv) {
1812
                $html .= ' (' . I18N::translate('%s year anniversary', I18N::number($fact->anniv)) . ')';
1813
            }
1814
            if (!$fact->getPlace()->isEmpty()) {
1815
                $html .= ' — <a href="' . $fact->getPlace()->getURL() . '">' . $fact->getPlace()->getFullName() . '</a>';
1816
            }
1817
            $html .= '</div>';
1818
        }
1819
1820
        // Print a final summary message about restricted/filtered facts
1821
        $summary = '';
1822
        if ($endjd == WT_CLIENT_JD) {
1823
            // We're dealing with the Today’s Events block
1824
            if ($output == 0) {
1825
                if ($filter == 0) {
1826
                    $summary = I18N::translate('No events exist for today.');
1827
                } else {
1828
                    $summary = I18N::translate('No events for living individuals exist for today.');
1829
                }
1830
            }
1831
        } else {
1832
            // We're dealing with the Upcoming Events block
1833
            if ($output == 0) {
1834
                if ($filter == 0) {
1835
                    if ($endjd == $startjd) {
1836
                        $summary = I18N::translate('No events exist for tomorrow.');
1837
                    } else {
1838
                        // I18N: translation for %s==1 is unused; it is translated separately as “tomorrow”
1839
                        $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));
1840
                    }
1841
                } else {
1842
                    if ($endjd == $startjd) {
1843
                        $summary = I18N::translate('No events for living individuals exist for tomorrow.');
1844
                    } else {
1845
                        // I18N: translation for %s==1 is unused; it is translated separately as “tomorrow”
1846
                        $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));
1847
                    }
1848
                }
1849
            }
1850
        }
1851
        if ($summary) {
1852
            $html .= '<b>' . $summary . '</b>';
1853
        }
1854
1855
        return $html;
1856
    }
1857
1858
    /**
1859
     * Print a chart by age using Google chart API
1860
     *
1861
     * @param int[] $data
1862
     * @param string $title
1863
     *
1864
     * @return string
1865
     */
1866
    public static function chartByAge($data, $title)
1867
    {
1868
        $count  = 0;
1869
        $agemax = 0;
1870
        $vmax   = 0;
1871
        $avg    = 0;
1872
        foreach ($data as $age => $v) {
1873
            $n      = strlen($v);
1874
            $vmax   = max($vmax, $n);
1875
            $agemax = max($agemax, $age);
1876
            $count += $n;
1877
            $avg += $age * $n;
1878
        }
1879
        if ($count < 1) {
1880
            return '';
1881
        }
1882
        $avg       = round($avg / $count);
1883
        $chart_url = "https://chart.googleapis.com/chart?cht=bvs"; // chart type
1884
        $chart_url .= "&amp;chs=725x150"; // size
1885
        $chart_url .= "&amp;chbh=3,2,2"; // bvg : 4,1,2
1886
        $chart_url .= "&amp;chf=bg,s,FFFFFF99"; //background color
1887
        $chart_url .= "&amp;chco=0000FF,FFA0CB,FF0000"; // bar color
1888
        $chart_url .= "&amp;chdl=" . rawurlencode(I18N::translate('Males')) . "|" . rawurlencode(I18N::translate('Females')) . "|" . rawurlencode(I18N::translate('Average age') . ": " . $avg); // legend & average age
1889
        $chart_url .= "&amp;chtt=" . rawurlencode($title); // title
1890
        $chart_url .= "&amp;chxt=x,y,r"; // axis labels specification
1891
        $chart_url .= "&amp;chm=V,FF0000,0," . ($avg - 0.3) . ",1"; // average age line marker
1892
        $chart_url .= "&amp;chxl=0:|"; // label
1893
        for ($age = 0; $age <= $agemax; $age += 5) {
1894
            $chart_url .= $age . "|||||"; // x axis
1895
        }
1896
        $chart_url .= "|1:||" . rawurlencode(I18N::percentage($vmax / $count)); // y axis
1897
        $chart_url .= "|2:||";
1898
        $step = $vmax;
1899
        for ($d = $vmax; $d > 0; $d--) {
1900
            if ($vmax < ($d * 10 + 1) && ($vmax % $d) == 0) {
1901
                $step = $d;
1902
            }
1903
        }
1904
        if ($step == $vmax) {
1905
            for ($d = $vmax - 1; $d > 0; $d--) {
1906
                if (($vmax - 1) < ($d * 10 + 1) && (($vmax - 1) % $d) == 0) {
1907
                    $step = $d;
1908
                }
1909
            }
1910
        }
1911
        for ($n = $step; $n < $vmax; $n += $step) {
1912
            $chart_url .= $n . "|";
1913
        }
1914
        $chart_url .= rawurlencode($vmax . " / " . $count); // r axis
1915
        $chart_url .= "&amp;chg=100," . round(100 * $step / $vmax, 1) . ",1,5"; // grid
1916
        $chart_url .= "&amp;chd=s:"; // data : simple encoding from A=0 to 9=61
1917
        $CHART_ENCODING61 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
1918
        for ($age = 0; $age <= $agemax; $age++) {
1919
            $chart_url .= $CHART_ENCODING61[(int) (substr_count($data[$age], "M") * 61 / $vmax)];
1920
        }
1921
        $chart_url .= ",";
1922
        for ($age = 0; $age <= $agemax; $age++) {
1923
            $chart_url .= $CHART_ENCODING61[(int) (substr_count($data[$age], "F") * 61 / $vmax)];
1924
        }
1925
        $html = '<img src="' . $chart_url . '" alt="' . $title . '" title="' . $title . '" class="gchart">';
1926
1927
        return $html;
1928
    }
1929
1930
    /**
1931
     * Print a chart by decade using Google chart API
1932
     *
1933
     * @param int[] $data
1934
     * @param string $title
1935
     *
1936
     * @return string
1937
     */
1938
    public static function chartByDecade($data, $title)
1939
    {
1940
        $count = 0;
1941
        $vmax  = 0;
1942
        foreach ($data as $v) {
1943
            $n    = strlen($v);
1944
            $vmax = max($vmax, $n);
1945
            $count += $n;
1946
        }
1947
        if ($count < 1) {
1948
            return '';
1949
        }
1950
        $chart_url = "https://chart.googleapis.com/chart?cht=bvs"; // chart type
1951
        $chart_url .= "&amp;chs=360x150"; // size
1952
        $chart_url .= "&amp;chbh=3,3"; // bvg : 4,1,2
1953
        $chart_url .= "&amp;chf=bg,s,FFFFFF99"; //background color
1954
        $chart_url .= "&amp;chco=0000FF,FFA0CB"; // bar color
1955
        $chart_url .= "&amp;chtt=" . rawurlencode($title); // title
1956
        $chart_url .= "&amp;chxt=x,y,r"; // axis labels specification
1957
        $chart_url .= "&amp;chxl=0:|&lt;|||"; // <1570
1958
        for ($y = 1600; $y < 2030; $y += 50) {
1959
            $chart_url .= $y . "|||||"; // x axis
1960
        }
1961
        $chart_url .= "|1:||" . rawurlencode(I18N::percentage($vmax / $count)); // y axis
1962
        $chart_url .= "|2:||";
1963
        $step = $vmax;
1964
        for ($d = $vmax; $d > 0; $d--) {
1965
            if ($vmax < ($d * 10 + 1) && ($vmax % $d) == 0) {
1966
                $step = $d;
1967
            }
1968
        }
1969
        if ($step == $vmax) {
1970
            for ($d = $vmax - 1; $d > 0; $d--) {
1971
                if (($vmax - 1) < ($d * 10 + 1) && (($vmax - 1) % $d) == 0) {
1972
                    $step = $d;
1973
                }
1974
            }
1975
        }
1976
        for ($n = $step; $n < $vmax; $n += $step) {
1977
            $chart_url .= $n . "|";
1978
        }
1979
        $chart_url .= rawurlencode($vmax . " / " . $count); // r axis
1980
        $chart_url .= "&amp;chg=100," . round(100 * $step / $vmax, 1) . ",1,5"; // grid
1981
        $chart_url .= "&amp;chd=s:"; // data : simple encoding from A=0 to 9=61
1982
        $CHART_ENCODING61 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
1983
        for ($y = 1570; $y < 2030; $y += 10) {
1984
            $chart_url .= $CHART_ENCODING61[(int) (substr_count($data[$y], "M") * 61 / $vmax)];
1985
        }
1986
        $chart_url .= ",";
1987
        for ($y = 1570; $y < 2030; $y += 10) {
1988
            $chart_url .= $CHART_ENCODING61[(int) (substr_count($data[$y], "F") * 61 / $vmax)];
1989
        }
1990
        $html = '<img src="' . $chart_url . '" alt="' . $title . '" title="' . $title . '" class="gchart">';
1991
1992
        return $html;
1993
    }
1994
}
1995