1
|
|
|
<?php |
2
|
|
|
/** |
3
|
|
|
* webtrees: online genealogy |
4
|
|
|
* Copyright (C) 2017 webtrees development team |
5
|
|
|
* This program is free software: you can redistribute it and/or modify |
6
|
|
|
* it under the terms of the GNU General Public License as published by |
7
|
|
|
* the Free Software Foundation, either version 3 of the License, or |
8
|
|
|
* (at your option) any later version. |
9
|
|
|
* This program is distributed in the hope that it will be useful, |
10
|
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of |
11
|
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
12
|
|
|
* GNU General Public License for more details. |
13
|
|
|
* You should have received a copy of the GNU General Public License |
14
|
|
|
* along with this program. If not, see <http://www.gnu.org/licenses/>. |
15
|
|
|
*/ |
16
|
|
|
namespace Fisharebest\Webtrees\Controller; |
17
|
|
|
|
18
|
|
|
use Fisharebest\Webtrees\Database; |
19
|
|
|
use Fisharebest\Webtrees\Family; |
20
|
|
|
use Fisharebest\Webtrees\Filter; |
21
|
|
|
use Fisharebest\Webtrees\Html; |
22
|
|
|
use Fisharebest\Webtrees\I18N; |
23
|
|
|
use Fisharebest\Webtrees\Individual; |
24
|
|
|
|
25
|
|
|
/** |
26
|
|
|
* Controller for the individual list |
27
|
|
|
*/ |
28
|
|
|
class IndividualListController extends PageController { |
29
|
|
|
/** |
30
|
|
|
* Get a list of initial letters, for lists of names. |
31
|
|
|
* |
32
|
|
|
* @param string $locale Return the alphabet for this locale |
33
|
|
|
* |
34
|
|
|
* @return string[] |
35
|
|
|
*/ |
36
|
|
View Code Duplication |
private function getAlphabetForLocale($locale) { |
|
|
|
|
37
|
|
|
switch ($locale) { |
38
|
|
|
case 'ar': |
39
|
|
|
return [ |
40
|
|
|
'ا', 'ب', 'ت', 'ث', 'ج', 'ح', 'خ', 'د', 'ذ', 'ر', 'ز', 'س', 'ش', 'ص', 'ض', 'ط', 'ظ', 'ع', 'غ', 'ف', 'ق', 'ك', 'ل', 'م', 'ن', 'ه', 'و', 'ي', 'آ', 'ة', 'ى', 'ی', |
41
|
|
|
]; |
42
|
|
|
case 'cs': |
43
|
|
|
return [ |
44
|
|
|
'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'CH', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', |
45
|
|
|
]; |
46
|
|
|
case 'da': |
47
|
|
|
case 'nb': |
48
|
|
|
case 'nn': |
49
|
|
|
return [ |
50
|
|
|
'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'Æ', 'Ø', 'Å', |
51
|
|
|
]; |
52
|
|
|
case 'el': |
53
|
|
|
return [ |
54
|
|
|
'Α', 'Β', 'Γ', 'Δ', 'Ε', 'Ζ', 'Η', 'Θ', 'Ι', 'Κ', 'Λ', 'Μ', 'Ν', 'Ξ', 'Ο', 'Π', 'Ρ', 'Σ', 'Τ', 'Υ', 'Φ', 'Χ', 'Ψ', 'Ω', |
55
|
|
|
]; |
56
|
|
|
case 'es': |
57
|
|
|
return [ |
58
|
|
|
'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'Ñ', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', |
59
|
|
|
]; |
60
|
|
|
case 'et': |
61
|
|
|
return [ |
62
|
|
|
'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'Š', 'Z', 'Ž', 'T', 'U', 'V', 'W', 'Õ', 'Ä', 'Ö', 'Ü', 'X', 'Y', |
63
|
|
|
]; |
64
|
|
|
case 'fi': |
65
|
|
|
case 'sv': |
66
|
|
|
return [ |
67
|
|
|
'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'Å', 'Ä', 'Ö', |
68
|
|
|
]; |
69
|
|
|
case 'he': |
70
|
|
|
return [ |
71
|
|
|
'א', 'ב', 'ג', 'ד', 'ה', 'ו', 'ז', 'ח', 'ט', 'י', 'כ', 'ל', 'מ', 'נ', 'ס', 'ע', 'פ', 'צ', 'ק', 'ר', 'ש', 'ת', |
72
|
|
|
]; |
73
|
|
|
case 'hu': |
74
|
|
|
return [ |
75
|
|
|
'A', 'B', 'C', 'CS', 'D', 'DZ', 'DZS', 'E', 'F', 'G', 'GY', 'H', 'I', 'J', 'K', 'L', 'LY', 'M', 'N', 'NY', 'O', 'Ö', 'P', 'Q', 'R', 'S', 'SZ', 'T', 'TY', 'U', 'Ü', 'V', 'W', 'X', 'Y', 'Z', 'ZS', |
76
|
|
|
]; |
77
|
|
|
case 'lt': |
78
|
|
|
return [ |
79
|
|
|
'A', 'Ą', 'B', 'C', 'Č', 'D', 'E', 'Ę', 'Ė', 'F', 'G', 'H', 'I', 'Y', 'Į', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'R', 'S', 'Š', 'T', 'U', 'Ų', 'Ū', 'V', 'Z', 'Ž', |
80
|
|
|
]; |
81
|
|
|
case 'nl': |
82
|
|
|
return [ |
83
|
|
|
'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'IJ', |
84
|
|
|
]; |
85
|
|
|
case 'pl': |
86
|
|
|
return [ |
87
|
|
|
'A', 'B', 'C', 'Ć', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'Ł', 'M', 'N', 'O', 'Ó', 'P', 'Q', 'R', 'S', 'Ś', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'Ź', 'Ż', |
88
|
|
|
]; |
89
|
|
|
case 'ro': |
90
|
|
|
return [ |
91
|
|
|
'A', 'Ă', 'Â', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'Î', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'Ş', 'T', 'Ţ', 'U', 'V', 'W', 'X', 'Y', 'Z', |
92
|
|
|
]; |
93
|
|
|
case 'ru': |
94
|
|
|
return [ |
95
|
|
|
'А', 'Б', 'В', 'Г', 'Д', 'Е', 'Ё', 'Ж', 'З', 'И', 'Й', 'К', 'Л', 'М', 'Н', 'О', 'П', 'Р', 'С', 'Т', 'У', 'Ф', 'Х', 'Ц', 'Ч', 'Ш', 'Щ', 'Ъ', 'Ы', 'Ь', 'Э', 'Ю', 'Я', |
96
|
|
|
]; |
97
|
|
|
case 'sk': |
98
|
|
|
return [ |
99
|
|
|
'A', 'Á', 'Ä', 'B', 'C', 'Č', 'D', 'Ď', 'E', 'É', 'F', 'G', 'H', 'I', 'Í', 'J', 'K', 'L', 'Ľ', 'Ĺ', 'M', 'N', 'Ň', 'O', 'Ó', 'Ô', 'P', 'Q', 'R', 'Ŕ', 'S', 'Š', 'T', 'Ť', 'U', 'Ú', 'V', 'W', 'X', 'Y', 'Ý', 'Z', 'Ž', |
100
|
|
|
]; |
101
|
|
|
case 'sl': |
102
|
|
|
return [ |
103
|
|
|
'A', 'B', 'C', 'Č', 'Ć', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'Š', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'Ž', |
104
|
|
|
]; |
105
|
|
|
case 'sr': |
106
|
|
|
return [ |
107
|
|
|
'A', 'B', 'C', 'Č', 'Ć', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'Š', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'Ž', |
108
|
|
|
]; |
109
|
|
|
case 'tr': |
110
|
|
|
return [ |
111
|
|
|
'A', 'B', 'C', 'Ç', 'D', 'E', 'F', 'G', 'Ğ', 'H', 'I', 'İ', 'J', 'K', 'L', 'M', 'N', 'O', 'Ö', 'P', 'R', 'S', 'Ş', 'T', 'U', 'Ü', 'V', 'Y', 'Z', |
112
|
|
|
]; |
113
|
|
|
default: |
114
|
|
|
return [ |
115
|
|
|
'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', |
116
|
|
|
]; |
117
|
|
|
} |
118
|
|
|
} |
119
|
|
|
|
120
|
|
|
/** |
121
|
|
|
* Get the initial letter of a name, taking care of multi-letter sequences and equivalences. |
122
|
|
|
* |
123
|
|
|
* @param string $name |
124
|
|
|
* |
125
|
|
|
* @return string |
126
|
|
|
*/ |
127
|
|
View Code Duplication |
public function initialLetter($name) { |
|
|
|
|
128
|
|
|
$name = I18N::strtoupper($name); |
129
|
|
|
switch (WT_LOCALE) { |
130
|
|
|
case 'cs': |
131
|
|
|
if (substr($name, 0, 2) == 'CH') { |
132
|
|
|
return 'CH'; |
133
|
|
|
} |
134
|
|
|
break; |
135
|
|
|
case 'da': |
136
|
|
|
case 'nb': |
137
|
|
|
case 'nn': |
138
|
|
|
if (substr($name, 0, 2) == 'AA') { |
139
|
|
|
return 'Å'; |
140
|
|
|
} |
141
|
|
|
break; |
142
|
|
|
case 'hu': |
143
|
|
|
if (substr($name, 0, 2) == 'CS') { |
144
|
|
|
return 'CS'; |
145
|
|
|
} elseif (substr($name, 0, 3) == 'DZS') { |
146
|
|
|
return 'DZS'; |
147
|
|
|
} elseif (substr($name, 0, 2) == 'DZ') { |
148
|
|
|
return 'DZ'; |
149
|
|
|
} elseif (substr($name, 0, 2) == 'GY') { |
150
|
|
|
return 'GY'; |
151
|
|
|
} elseif (substr($name, 0, 2) == 'LY') { |
152
|
|
|
return 'LY'; |
153
|
|
|
} elseif (substr($name, 0, 2) == 'NY') { |
154
|
|
|
return 'NY'; |
155
|
|
|
} elseif (substr($name, 0, 2) == 'SZ') { |
156
|
|
|
return 'SZ'; |
157
|
|
|
} elseif (substr($name, 0, 2) == 'TY') { |
158
|
|
|
return 'TY'; |
159
|
|
|
} elseif (substr($name, 0, 2) == 'ZS') { |
160
|
|
|
return 'ZS'; |
161
|
|
|
} |
162
|
|
|
break; |
163
|
|
|
case 'nl': |
164
|
|
|
if (substr($name, 0, 2) == 'IJ') { |
165
|
|
|
return 'IJ'; |
166
|
|
|
} |
167
|
|
|
break; |
168
|
|
|
} |
169
|
|
|
// No special rules - just take the first character |
170
|
|
|
return mb_substr($name, 0, 1); |
171
|
|
|
} |
172
|
|
|
|
173
|
|
|
/** |
174
|
|
|
* Generate SQL to match a given letter, taking care of cases that |
175
|
|
|
* are not covered by the collation setting. |
176
|
|
|
* |
177
|
|
|
* We must consider: |
178
|
|
|
* potential substrings, such as Czech "CH" and "C" |
179
|
|
|
* equivalent letters, such as Danish "AA" and "Å" |
180
|
|
|
* |
181
|
|
|
* We COULD write something that handles all languages generically, |
182
|
|
|
* but its performance would most likely be poor. |
183
|
|
|
* |
184
|
|
|
* For languages that don't appear in this list, we could write |
185
|
|
|
* simpler versions of the surnameAlpha() and givenAlpha() functions, |
186
|
|
|
* but it gives no noticable improvement in performance. |
187
|
|
|
* |
188
|
|
|
* @param string $field |
189
|
|
|
* @param string $letter |
190
|
|
|
* |
191
|
|
|
* @return string |
192
|
|
|
*/ |
193
|
|
View Code Duplication |
private function getInitialSql($field, $letter) { |
|
|
|
|
194
|
|
|
switch (WT_LOCALE) { |
195
|
|
|
case 'cs': |
196
|
|
|
switch ($letter) { |
197
|
|
|
case 'C': return $field . " LIKE 'C%' COLLATE " . I18N::collation() . " AND " . $field . " NOT LIKE 'CH%' COLLATE " . I18N::collation(); |
198
|
|
|
} |
199
|
|
|
break; |
200
|
|
|
case 'da': |
201
|
|
|
case 'nb': |
202
|
|
|
case 'nn': |
203
|
|
|
switch ($letter) { |
204
|
|
|
// AA gets listed under Å |
205
|
|
|
case 'A': return $field . " LIKE 'A%' COLLATE " . I18N::collation() . " AND " . $field . " NOT LIKE 'AA%' COLLATE " . I18N::collation(); |
206
|
|
|
case 'Å': return "(" . $field . " LIKE 'Å%' COLLATE " . I18N::collation() . " OR " . $field . " LIKE 'AA%' COLLATE " . I18N::collation() . ")"; |
207
|
|
|
} |
208
|
|
|
break; |
209
|
|
|
case 'hu': |
210
|
|
|
switch ($letter) { |
211
|
|
|
case 'C': return $field . " LIKE 'C%' COLLATE " . I18N::collation() . " AND " . $field . " NOT LIKE 'CS%' COLLATE " . I18N::collation(); |
212
|
|
|
case 'D': return $field . " LIKE 'D%' COLLATE " . I18N::collation() . " AND " . $field . " NOT LIKE 'DZ%' COLLATE " . I18N::collation(); |
213
|
|
|
case 'DZ': return $field . " LIKE 'DZ%' COLLATE " . I18N::collation() . " AND " . $field . " NOT LIKE 'DZS%' COLLATE " . I18N::collation(); |
214
|
|
|
case 'G': return $field . " LIKE 'G%' COLLATE " . I18N::collation() . " AND " . $field . " NOT LIKE 'GY%' COLLATE " . I18N::collation(); |
215
|
|
|
case 'L': return $field . " LIKE 'L%' COLLATE " . I18N::collation() . " AND " . $field . " NOT LIKE 'LY%' COLLATE " . I18N::collation(); |
216
|
|
|
case 'N': return $field . " LIKE 'N%' COLLATE " . I18N::collation() . " AND " . $field . " NOT LIKE 'NY%' COLLATE " . I18N::collation(); |
217
|
|
|
case 'S': return $field . " LIKE 'S%' COLLATE " . I18N::collation() . " AND " . $field . " NOT LIKE 'SZ%' COLLATE " . I18N::collation(); |
218
|
|
|
case 'T': return $field . " LIKE 'T%' COLLATE " . I18N::collation() . " AND " . $field . " NOT LIKE 'TY%' COLLATE " . I18N::collation(); |
219
|
|
|
case 'Z': return $field . " LIKE 'Z%' COLLATE " . I18N::collation() . " AND " . $field . " NOT LIKE 'ZS%' COLLATE " . I18N::collation(); |
220
|
|
|
} |
221
|
|
|
break; |
222
|
|
|
case 'nl': |
223
|
|
|
switch ($letter) { |
224
|
|
|
case 'I': return $field . " LIKE 'I%' COLLATE " . I18N::collation() . " AND " . $field . " NOT LIKE 'IJ%' COLLATE " . I18N::collation(); |
225
|
|
|
} |
226
|
|
|
break; |
227
|
|
|
} |
228
|
|
|
|
229
|
|
|
// Easy cases: the MySQL collation rules take care of it |
230
|
|
|
return "$field LIKE CONCAT('@'," . Database::quote($letter) . ",'%') COLLATE " . I18N::collation() . " ESCAPE '@'"; |
|
|
|
|
231
|
|
|
} |
232
|
|
|
|
233
|
|
|
/** |
234
|
|
|
* Get a list of initial surname letters for indilist.php and famlist.php |
235
|
|
|
* |
236
|
|
|
* @param bool $marnm if set, include married names |
237
|
|
|
* @param bool $fams if set, only consider individuals with FAMS records |
238
|
|
|
* @param bool $totals if set, count the number of names beginning with each letter |
239
|
|
|
* |
240
|
|
|
* @return int[] |
241
|
|
|
*/ |
242
|
|
|
public function surnameAlpha($marnm, $fams, $totals = true) { |
243
|
|
|
$alphas = []; |
244
|
|
|
|
245
|
|
|
$sql = |
246
|
|
|
"SELECT SQL_CACHE COUNT(n_id)" . |
247
|
|
|
" FROM `##name` " . |
248
|
|
|
($fams ? " JOIN `##link` ON (n_id=l_from AND n_file=l_file AND l_type='FAMS') " : "") . |
249
|
|
|
" WHERE n_file=" . $this->tree()->getTreeId() . |
250
|
|
|
($marnm ? "" : " AND n_type!='_MARNM'"); |
251
|
|
|
|
252
|
|
|
// Fetch all the letters in our alphabet, whether or not there |
253
|
|
|
// are any names beginning with that letter. It looks better to |
254
|
|
|
// show the full alphabet, rather than omitting rare letters such as X |
255
|
|
View Code Duplication |
foreach ($this->getAlphabetForLocale(WT_LOCALE) as $letter) { |
256
|
|
|
$count = 1; |
257
|
|
|
if ($totals) { |
258
|
|
|
$count = Database::prepare($sql . " AND " . $this->getInitialSql('n_surn', $letter))->fetchOne(); |
259
|
|
|
} |
260
|
|
|
$alphas[$letter] = $count; |
261
|
|
|
} |
262
|
|
|
|
263
|
|
|
// Now fetch initial letters that are not in our alphabet, |
264
|
|
|
// including "@" (for "@N.N.") and "" for no surname. |
265
|
|
|
$sql = |
266
|
|
|
"SELECT SQL_CACHE initial, count FROM (SELECT UPPER(LEFT(n_surn, 1)) AS initial, COUNT(n_id) AS count" . |
267
|
|
|
" FROM `##name` " . |
268
|
|
|
($fams ? " JOIN `##link` ON n_id = l_from AND n_file = l_file AND l_type = 'FAMS' " : "") . |
269
|
|
|
" WHERE n_file = :tree_id AND n_surn <> ''" . |
270
|
|
|
($marnm ? "" : " AND n_type != '_MARNM'"); |
271
|
|
|
|
272
|
|
|
$args = [ |
273
|
|
|
'tree_id' => $this->tree()->getTreeId(), |
274
|
|
|
]; |
275
|
|
|
|
276
|
|
|
foreach ($this->getAlphabetForLocale(WT_LOCALE) as $n => $letter) { |
277
|
|
|
$sql .= " AND n_surn COLLATE :collate_" . $n . " NOT LIKE :letter_" . $n; |
278
|
|
|
$args['collate_' . $n] = I18N::collation(); |
279
|
|
|
$args['letter_' . $n] = $letter . '%'; |
280
|
|
|
} |
281
|
|
|
$sql .= " GROUP BY UPPER(LEFT(n_surn, 1))) AS subquery ORDER BY initial = '', initial = '@', initial"; |
282
|
|
|
foreach (Database::prepare($sql)->execute($args)->fetchAssoc() as $alpha => $count) { |
283
|
|
|
$alphas[$alpha] = $count; |
284
|
|
|
} |
285
|
|
|
|
286
|
|
|
// Names with no surname |
287
|
|
|
$sql = |
288
|
|
|
"SELECT SQL_CACHE COUNT(n_id)" . |
289
|
|
|
" FROM `##name` " . |
290
|
|
|
($fams ? " JOIN `##link` ON n_id = l_from AND n_file = l_file AND l_type = 'FAMS' " : "") . |
291
|
|
|
" WHERE n_file = :tree_id AND n_surn = ''" . |
292
|
|
|
($marnm ? "" : " AND n_type != '_MARNM'"); |
293
|
|
|
|
294
|
|
|
$args = [ |
295
|
|
|
'tree_id' => $this->tree()->getTreeId(), |
296
|
|
|
]; |
297
|
|
|
|
298
|
|
|
$count_no_surname = (int) Database::prepare($sql)->execute($args)->fetchOne(); |
299
|
|
|
if ($count_no_surname !== 0) { |
300
|
|
|
// Special code to indicate "no surname" |
301
|
|
|
$alphas[','] = $count_no_surname; |
302
|
|
|
} |
303
|
|
|
|
304
|
|
|
return $alphas; |
305
|
|
|
} |
306
|
|
|
|
307
|
|
|
/** |
308
|
|
|
* Get a list of initial given name letters for indilist.php and famlist.php |
309
|
|
|
* |
310
|
|
|
* @param string $surn if set, only consider people with this surname |
311
|
|
|
* @param string $salpha if set, only consider surnames starting with this letter |
312
|
|
|
* @param bool $marnm if set, include married names |
313
|
|
|
* @param bool $fams if set, only consider individuals with FAMS records |
314
|
|
|
* |
315
|
|
|
* @return int[] |
316
|
|
|
*/ |
317
|
|
|
public function givenAlpha($surn, $salpha, $marnm, $fams) { |
318
|
|
|
$alphas = []; |
319
|
|
|
|
320
|
|
|
$sql = |
321
|
|
|
"SELECT SQL_CACHE COUNT(DISTINCT n_id)" . |
322
|
|
|
" FROM `##name`" . |
323
|
|
|
($fams ? " JOIN `##link` ON (n_id=l_from AND n_file=l_file AND l_type='FAMS') " : "") . |
324
|
|
|
" WHERE n_file=" . $this->tree()->getTreeId() . " " . |
325
|
|
|
($marnm ? "" : " AND n_type!='_MARNM'"); |
326
|
|
|
|
327
|
|
View Code Duplication |
if ($surn) { |
328
|
|
|
$sql .= " AND n_surn=" . Database::quote($surn) . " COLLATE '" . I18N::collation() . "'"; |
|
|
|
|
329
|
|
|
} elseif ($salpha == ',') { |
330
|
|
|
$sql .= " AND n_surn=''"; |
331
|
|
|
} elseif ($salpha == '@') { |
332
|
|
|
$sql .= " AND n_surn='@N.N.'"; |
333
|
|
|
} elseif ($salpha) { |
334
|
|
|
$sql .= " AND " . $this->getInitialSql('n_surn', $salpha); |
335
|
|
|
} else { |
336
|
|
|
// All surnames |
337
|
|
|
$sql .= " AND n_surn NOT IN ('', '@N.N.')"; |
338
|
|
|
} |
339
|
|
|
|
340
|
|
|
// Fetch all the letters in our alphabet, whether or not there |
341
|
|
|
// are any names beginning with that letter. It looks better to |
342
|
|
|
// show the full alphabet, rather than omitting rare letters such as X |
343
|
|
View Code Duplication |
foreach ($this->getAlphabetForLocale(WT_LOCALE) as $letter) { |
344
|
|
|
$count = Database::prepare($sql . " AND " . $this->getInitialSql('n_givn', $letter))->fetchOne(); |
345
|
|
|
$alphas[$letter] = $count; |
346
|
|
|
} |
347
|
|
|
|
348
|
|
|
// Now fetch initial letters that are not in our alphabet, |
349
|
|
|
// including "@" (for "@N.N.") and "" for no surname |
350
|
|
|
$sql = |
351
|
|
|
"SELECT SQL_CACHE initial, total FROM (SELECT UPPER(LEFT(n_givn, 1)) AS initial, COUNT(DISTINCT n_id) AS total" . |
352
|
|
|
" FROM `##name` " . |
353
|
|
|
($fams ? " JOIN `##link` ON (n_id = l_from AND n_file = l_file AND l_type = 'FAMS') " : "") . |
354
|
|
|
" WHERE n_file = :tree_id" . |
355
|
|
|
($marnm ? "" : " AND n_type != '_MARNM'"); |
356
|
|
|
|
357
|
|
|
$args = [ |
358
|
|
|
'tree_id' => $this->tree()->getTreeId(), |
359
|
|
|
]; |
360
|
|
|
|
361
|
|
View Code Duplication |
if ($surn) { |
362
|
|
|
$sql .= " AND n_surn COLLATE :collate_1 = :surn"; |
363
|
|
|
$args['collate_1'] = I18N::collation(); |
364
|
|
|
$args['surn'] = $surn; |
365
|
|
|
} elseif ($salpha === ',') { |
366
|
|
|
$sql .= " AND n_surn = ''"; |
367
|
|
|
} elseif ($salpha === '@') { |
368
|
|
|
$sql .= " AND n_surn = '@N.N.'"; |
369
|
|
|
} elseif ($salpha) { |
370
|
|
|
$sql .= " AND " . $this->getInitialSql('n_surn', $salpha); |
371
|
|
|
} else { |
372
|
|
|
// All surnames |
373
|
|
|
$sql .= " AND n_surn NOT IN ('', '@N.N.')"; |
374
|
|
|
} |
375
|
|
|
|
376
|
|
View Code Duplication |
foreach ($this->getAlphabetForLocale(WT_LOCALE) as $letter) { |
377
|
|
|
$sql .= " AND n_givn NOT LIKE '" . $letter . "%' COLLATE " . I18N::collation(); |
378
|
|
|
} |
379
|
|
|
$sql .= " GROUP BY UPPER(LEFT(n_givn, 1))) AS subquery ORDER BY initial = '@', initial = '', initial"; |
380
|
|
|
|
381
|
|
|
foreach (Database::prepare($sql)->execute($args)->fetchAssoc() as $alpha => $count) { |
382
|
|
|
$alphas[$alpha] = $count; |
383
|
|
|
} |
384
|
|
|
|
385
|
|
|
return $alphas; |
386
|
|
|
} |
387
|
|
|
|
388
|
|
|
/** |
389
|
|
|
* Get a list of actual surnames and variants, based on a "root" surname. |
390
|
|
|
* |
391
|
|
|
* @param string $surn if set, only fetch people with this surname |
392
|
|
|
* @param string $salpha if set, only consider surnames starting with this letter |
393
|
|
|
* @param bool $marnm if set, include married names |
394
|
|
|
* @param bool $fams if set, only consider individuals with FAMS records |
395
|
|
|
* |
396
|
|
|
* @return array |
397
|
|
|
*/ |
398
|
|
|
public function surnames($surn, $salpha, $marnm, $fams) { |
399
|
|
|
$sql = |
400
|
|
|
"SELECT SQL_CACHE n2.n_surn, n1.n_surname, n1.n_id" . |
401
|
|
|
" FROM `##name` n1 " . |
402
|
|
|
($fams ? " JOIN `##link` ON n_id = l_from AND n_file = l_file AND l_type = 'FAMS' " : "") . |
403
|
|
|
" JOIN (SELECT n_surn COLLATE :collate_0 AS n_surn, n_file FROM `##name`" . |
404
|
|
|
" WHERE n_file = :tree_id" . |
405
|
|
|
($marnm ? "" : " AND n_type != '_MARNM'"); |
406
|
|
|
|
407
|
|
|
$args = [ |
408
|
|
|
'tree_id' => $this->tree()->getTreeId(), |
409
|
|
|
'collate_0' => I18N::collation(), |
410
|
|
|
]; |
411
|
|
|
|
412
|
|
View Code Duplication |
if ($surn) { |
413
|
|
|
$sql .= " AND n_surn COLLATE :collate_1 = :surn"; |
414
|
|
|
$args['collate_1'] = I18N::collation(); |
415
|
|
|
$args['surn'] = $surn; |
416
|
|
|
} elseif ($salpha === ',') { |
417
|
|
|
$sql .= " AND n_surn = ''"; |
418
|
|
|
} elseif ($salpha === '@') { |
419
|
|
|
$sql .= " AND n_surn = '@N.N.'"; |
420
|
|
|
} elseif ($salpha) { |
421
|
|
|
$sql .= " AND " . $this->getInitialSql('n_surn', $salpha); |
422
|
|
|
} else { |
423
|
|
|
// All surnames |
424
|
|
|
$sql .= " AND n_surn NOT IN ('', '@N.N.')"; |
425
|
|
|
} |
426
|
|
|
$sql .= " GROUP BY n_surn COLLATE :collate_2, n_file) AS n2 ON (n1.n_surn = n2.n_surn COLLATE :collate_3 AND n1.n_file = n2.n_file)"; |
427
|
|
|
$args['collate_2'] = I18N::collation(); |
428
|
|
|
$args['collate_3'] = I18N::collation(); |
429
|
|
|
|
430
|
|
|
$list = []; |
431
|
|
View Code Duplication |
foreach (Database::prepare($sql)->execute($args)->fetchAll() as $row) { |
432
|
|
|
$list[I18N::strtoupper($row->n_surn)][$row->n_surname][$row->n_id] = true; |
433
|
|
|
} |
434
|
|
|
|
435
|
|
|
return $list; |
436
|
|
|
} |
437
|
|
|
|
438
|
|
|
/** |
439
|
|
|
* Fetch a list of individuals with specified names |
440
|
|
|
* |
441
|
|
|
* To search for unknown names, use $surn="@N.N.", $salpha="@" or $galpha="@" |
442
|
|
|
* To search for names with no surnames, use $salpha="," |
443
|
|
|
* |
444
|
|
|
* @param string $surn if set, only fetch people with this surname |
445
|
|
|
* @param string $salpha if set, only fetch surnames starting with this letter |
446
|
|
|
* @param string $galpha if set, only fetch given names starting with this letter |
447
|
|
|
* @param bool $marnm if set, include married names |
448
|
|
|
* @param bool $fams if set, only fetch individuals with FAMS records |
449
|
|
|
* |
450
|
|
|
* @return Individual[] |
451
|
|
|
*/ |
452
|
|
|
public function individuals($surn, $salpha, $galpha, $marnm, $fams) { |
453
|
|
|
$sql = |
454
|
|
|
"SELECT i_id AS xref, i_gedcom AS gedcom, n_full " . |
455
|
|
|
"FROM `##individuals` " . |
456
|
|
|
"JOIN `##name` ON n_id = i_id AND n_file = i_file " . |
457
|
|
|
($fams ? "JOIN `##link` ON n_id = l_from AND n_file = l_file AND l_type = 'FAMS' " : "") . |
458
|
|
|
"WHERE n_file = :tree_id " . |
459
|
|
|
($marnm ? "" : "AND n_type != '_MARNM'"); |
460
|
|
|
|
461
|
|
|
$args = [ |
462
|
|
|
'tree_id' => $this->tree()->getTreeId(), |
463
|
|
|
]; |
464
|
|
|
|
465
|
|
View Code Duplication |
if ($surn) { |
466
|
|
|
$sql .= " AND n_surn COLLATE :collate_1 = :surn"; |
467
|
|
|
$args['collate_1'] = I18N::collation(); |
468
|
|
|
$args['surn'] = $surn; |
469
|
|
|
} elseif ($salpha === ',') { |
470
|
|
|
$sql .= " AND n_surn = ''"; |
471
|
|
|
} elseif ($salpha === '@') { |
472
|
|
|
$sql .= " AND n_surn = '@N.N.'"; |
473
|
|
|
} elseif ($salpha) { |
474
|
|
|
$sql .= " AND " . $this->getInitialSql('n_surn', $salpha); |
475
|
|
|
} else { |
476
|
|
|
// All surnames |
477
|
|
|
$sql .= " AND n_surn NOT IN ('', '@N.N.')"; |
478
|
|
|
} |
479
|
|
|
if ($galpha) { |
480
|
|
|
$sql .= " AND " . $this->getInitialSql('n_givn', $galpha); |
481
|
|
|
} |
482
|
|
|
|
483
|
|
|
$sql .= " ORDER BY CASE n_surn WHEN '@N.N.' THEN 1 ELSE 0 END, n_surn COLLATE :collate_2, CASE n_givn WHEN '@P.N.' THEN 1 ELSE 0 END, n_givn COLLATE :collate_3"; |
484
|
|
|
$args['collate_2'] = I18N::collation(); |
485
|
|
|
$args['collate_3'] = I18N::collation(); |
486
|
|
|
|
487
|
|
|
$list = []; |
488
|
|
|
$rows = Database::prepare($sql)->execute($args)->fetchAll(); |
489
|
|
|
foreach ($rows as $row) { |
490
|
|
|
$person = Individual::getInstance($row->xref, $this->tree(), $row->gedcom); |
491
|
|
|
// The name from the database may be private - check the filtered list... |
492
|
|
|
foreach ($person->getAllNames() as $n => $name) { |
493
|
|
|
if ($name['fullNN'] == $row->n_full) { |
494
|
|
|
$person->setPrimaryName($n); |
495
|
|
|
// We need to clone $person, as we may have multiple references to the |
496
|
|
|
// same person in this list, and the "primary name" would otherwise |
497
|
|
|
// be shared amongst all of them. |
498
|
|
|
$list[] = clone $person; |
499
|
|
|
break; |
500
|
|
|
} |
501
|
|
|
} |
502
|
|
|
} |
503
|
|
|
|
504
|
|
|
return $list; |
505
|
|
|
} |
506
|
|
|
|
507
|
|
|
/** |
508
|
|
|
* Fetch a list of families with specified names |
509
|
|
|
* |
510
|
|
|
* To search for unknown names, use $surn="@N.N.", $salpha="@" or $galpha="@" |
511
|
|
|
* To search for names with no surnames, use $salpha="," |
512
|
|
|
* |
513
|
|
|
* @param string $surn if set, only fetch people with this surname |
514
|
|
|
* @param string $salpha if set, only fetch surnames starting with this letter |
515
|
|
|
* @param string $galpha if set, only fetch given names starting with this letter |
516
|
|
|
* @param bool $marnm if set, include married names |
517
|
|
|
* |
518
|
|
|
* @return Family[] |
519
|
|
|
*/ |
520
|
|
View Code Duplication |
public function families($surn, $salpha, $galpha, $marnm) { |
|
|
|
|
521
|
|
|
$list = []; |
522
|
|
|
foreach ($this->individuals($surn, $salpha, $galpha, $marnm, true) as $indi) { |
523
|
|
|
foreach ($indi->getSpouseFamilies() as $family) { |
524
|
|
|
$list[$family->getXref()] = $family; |
525
|
|
|
} |
526
|
|
|
} |
527
|
|
|
usort($list, '\Fisharebest\Webtrees\GedcomRecord::compare'); |
528
|
|
|
|
529
|
|
|
return $list; |
530
|
|
|
} |
531
|
|
|
|
532
|
|
|
/** |
533
|
|
|
* Some initial letters have a special meaning |
534
|
|
|
* |
535
|
|
|
* @param string $initial |
536
|
|
|
* |
537
|
|
|
* @return string |
538
|
|
|
*/ |
539
|
|
View Code Duplication |
public function givenNameInitial($initial) { |
|
|
|
|
540
|
|
|
switch ($initial) { |
541
|
|
|
case '@': |
542
|
|
|
return I18N::translateContext('Unknown given name', '…'); |
543
|
|
|
break; |
|
|
|
|
544
|
|
|
default: |
545
|
|
|
return Html::escape($initial); |
546
|
|
|
break; |
547
|
|
|
} |
548
|
|
|
} |
549
|
|
|
|
550
|
|
|
/** |
551
|
|
|
* Some initial letters have a special meaning |
552
|
|
|
* |
553
|
|
|
* @param string $initial |
554
|
|
|
* |
555
|
|
|
* @return string |
556
|
|
|
*/ |
557
|
|
View Code Duplication |
public function surnameInitial($initial) { |
|
|
|
|
558
|
|
|
switch ($initial) { |
559
|
|
|
case '@': |
560
|
|
|
return I18N::translateContext('Unknown surname', '…'); |
561
|
|
|
break; |
|
|
|
|
562
|
|
|
case ',': |
563
|
|
|
return I18N::translate('None'); |
564
|
|
|
break; |
565
|
|
|
default: |
566
|
|
|
return Html::escape($initial); |
567
|
|
|
break; |
568
|
|
|
} |
569
|
|
|
} |
570
|
|
|
} |
571
|
|
|
|
Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.
You can also find more detailed suggestions in the “Code” section of your repository.