10 |
11 |
12 | = I18N::translate('There are various data fixes available.'). ' ' ?>
13 | = I18N::translate('It is strongly recommended to backup your tree first.') ?>
14 |
15 |
16 |
145 |
--------------------------------------------------------------------------------
/patchedWebtrees/Elements/LanguageIdExt.php:
--------------------------------------------------------------------------------
1 |
89 | */
90 | public static function values(): array
91 | {
92 | $values = [
93 | //'' => '',
94 | 'Afrikaans' => (new LocaleAf()),
95 | 'Albanian' => (new LocaleSq()),
96 | 'Amharic' => (new LocaleAm()),
97 | 'Anglo-Saxon' => (new LocaleAng()),
98 | 'Arabic' => (new LocaleAr()),
99 | 'Armenian' => (new LocaleHy()),
100 | 'Assamese' => (new LocaleAs()),
101 | 'Belorusian' => (new LocaleBe()),
102 | 'Bengali' => (new LocaleBn()),
103 | //'Braj' => (new LocaleBra()),
104 | 'Bulgarian' => (new LocaleBg()),
105 | 'Burmese' => (new LocaleMy()),
106 | 'Cantonese' => (new LocaleYue()),
107 |
108 | //[RC] adjusted
109 | //'Catalan' => (new LocaleCaEsValencia()),
110 | //'Catalan_Spn' => (new LocaleCa()),
111 | 'Catalan' => (new LocaleCa()),
112 |
113 | 'Church-Slavic' => (new LocaleCu()),
114 | 'Czech' => (new LocaleCs()),
115 | 'Danish' => (new LocaleDa()),
116 | //'Dogri' => (new LocaleDoi()),
117 | 'Dutch' => (new LocaleNl()),
118 | 'English' => (new LocaleEn()),
119 | 'Esperanto' => (new LocaleEo()),
120 | 'Estonian' => (new LocaleEt()),
121 | 'Faroese' => (new LocaleFo()),
122 | 'Finnish' => (new LocaleFi()),
123 | 'French' => (new LocaleFr()),
124 | 'Georgian' => (new LocaleKa()),
125 | 'German' => (new LocaleDe()),
126 | 'Greek' => (new LocaleEl()),
127 | 'Gujarati' => (new LocaleGu()),
128 | 'Hawaiian' => (new LocaleHaw()),
129 | 'Hebrew' => (new LocaleHe()),
130 | 'Hindi' => (new LocaleHi()),
131 | 'Hungarian' => (new LocaleHu()),
132 | 'Icelandic' => (new LocaleIs()),
133 | 'Indonesian' => (new LocaleId()),
134 | 'Italian' => (new LocaleIt()),
135 | 'Japanese' => (new LocaleJa()),
136 | 'Kannada' => (new LocaleKn()),
137 | 'Khmer' => (new LocaleKm()),
138 | 'Konkani' => (new LocaleKok()),
139 | 'Korean' => (new LocaleKo()),
140 | //'Lahnda' => (new LocaleLah()),
141 | 'Lao' => (new LocaleLo()),
142 | 'Latvian' => (new LocaleLv()),
143 | 'Lithuanian' => (new LocaleLt()),
144 | 'Macedonian' => (new LocaleMk()),
145 | //'Maithili' => (new LocaleMai()),
146 | 'Malayalam' => (new LocaleMl()),
147 | //'Mandrin' => (new LocaleCmn()),
148 | //'Manipuri' => (new LocaleMni()),
149 | 'Marathi' => (new LocaleMr()),
150 | //'Mewari' => (new LocaleMtr()),
151 | //'Navaho' => (new LocaleNv()),
152 | 'Nepali' => (new LocaleNe()),
153 | 'Norwegian' => (new LocaleNn()),
154 | 'Oriya' => (new LocaleOr()),
155 | //'Pahari' => (new LocalePhr()),
156 | //'Pali' => (new LocalePi()),
157 | 'Panjabi' => (new LocalePa()),
158 | 'Persian' => (new LocaleFa()),
159 | 'Polish' => (new LocalePl()),
160 | 'Portuguese' => (new LocalePt()),
161 | //'Prakrit' => (new LocalePra()),
162 | 'Pusto' => (new LocalePs()),
163 | //'Rajasthani' => (new LocaleRaj()),
164 | 'Romanian' => (new LocaleRo()),
165 | 'Russian' => (new LocaleRu()),
166 | //'Sanskrit' => (new LocaleSa()),
167 | 'Serb' => (new LocaleSr()),
168 | //'Serbo_Croa' => (new LocaleHbs()),
169 | 'Slovak' => (new LocaleSk()),
170 | 'Slovene' => (new LocaleSl()),
171 | 'Spanish' => (new LocaleEs()),
172 | 'Swedish' => (new LocaleSv()),
173 | 'Tagalog' => (new LocaleTl()),
174 | 'Tamil' => (new LocaleTa()),
175 | 'Telugu' => (new LocaleTe()),
176 | 'Thai' => (new LocaleTh()),
177 | 'Tibetan' => (new LocaleBo()),
178 | 'Turkish' => (new LocaleTr()),
179 | 'Ukrainian' => (new LocaleUk()),
180 | 'Urdu' => (new LocaleUr()),
181 | 'Vietnamese' => (new LocaleVi()),
182 | //'Wendic' => (new LocaleWen()),
183 | 'Yiddish' => (new LocaleYi()),
184 | ];
185 |
186 | return $values;
187 | }
188 |
189 | public static function valuesWithUpperCasedKeys(): array {
190 | $locales = LanguageIdExt::values();
191 |
192 | $coll = new Collection($locales);
193 |
194 | $values = $coll
195 | ->mapWithKeys(static function (LocaleInterface $locale, string $key): array {
196 | return [strtoupper($key) => $locale];
197 | })
198 | ->all();
199 |
200 | return $values;
201 | }
202 |
203 | }
204 |
--------------------------------------------------------------------------------
/resources/views/js/webtreesExt.phtml:
--------------------------------------------------------------------------------
1 |
4 |
5 |
223 |
--------------------------------------------------------------------------------
/metadata.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "version": "2.0.13.0.0",
4 | "from": "2.0.12",
5 | "to": "2.0.18"
6 | },
7 | {
8 | "version": "2.0.13.1.0",
9 | "from": "2.0.12",
10 | "to": "2.0.18"
11 | },
12 | {
13 | "version": "2.0.15.0.0",
14 | "from": "2.0.12",
15 | "to": "2.0.18"
16 | },
17 | {
18 | "version": "2.0.15.1.0",
19 | "from": "2.0.12",
20 | "to": "2.0.18"
21 | },
22 | {
23 | "version": "2.0.15.2.0",
24 | "from": "2.0.12",
25 | "to": "2.0.18"
26 | },
27 | {
28 | "version": "2.0.15.3.0",
29 | "from": "2.0.12",
30 | "to": "2.0.18"
31 | },
32 | {
33 | "version": "2.0.15.4.0",
34 | "from": "2.0.12",
35 | "to": "2.0.18",
36 | "changelog": ["Option to set a reference date for the summary on the shared place page."]
37 | },
38 | {
39 | "version": "2.0.16.0.0",
40 | "from": "2.0.12",
41 | "to": "2.0.18"
42 | },
43 | {
44 | "version": "2.0.16.0.1",
45 | "from": "2.0.12",
46 | "to": "2.0.18",
47 | "changelog": ["Bugfix."]
48 | },
49 | {
50 | "version": "2.0.16.0.2",
51 | "from": "2.0.12",
52 | "to": "2.0.18",
53 | "changelog": ["Bugfix."]
54 | },
55 | {
56 | "version": "2.0.16.0.3",
57 | "from": "2.0.12",
58 | "to": "2.0.18"
59 | },
60 | {
61 | "version": "2.0.16.0.4",
62 | "from": "2.0.12",
63 | "to": "2.0.18",
64 | "changelog": ["Small Bugfix."]
65 | },
66 | {
67 | "version": "2.0.16.1.0",
68 | "from": "2.0.12",
69 | "to": "2.0.18"
70 | },
71 | {
72 | "version": "2.0.16.1.1",
73 | "from": "2.0.12",
74 | "to": "2.0.18",
75 | "changelog": ["Display place names in the user's language if possible."]
76 | },
77 | {
78 | "version": "2.0.16.2.0",
79 | "from": "2.0.12",
80 | "to": "2.0.18"
81 | },
82 | {
83 | "version": "2.0.16.3.0",
84 | "from": "2.0.12",
85 | "to": "2.0.18",
86 | "changelog": ["Support shared places in general search."]
87 | },
88 | {
89 | "version": "2.0.16.4.0",
90 | "from": "2.0.12",
91 | "to": "2.0.18"
92 | },
93 | {
94 | "version": "2.0.16.5.0",
95 | "from": "2.0.12",
96 | "to": "2.0.18"
97 | },
98 | {
99 | "version": "2.0.16.5.2",
100 | "from": "2.0.12",
101 | "to": "2.0.18"
102 | },
103 | {
104 | "version": "2.0.16.6.0",
105 | "from": "2.0.12",
106 | "to": "2.0.18"
107 | },
108 | {
109 | "version": "2.0.16.6.2",
110 | "from": "2.0.12",
111 | "to": "2.0.18"
112 | },
113 | {
114 | "version": "2.0.17.0.0",
115 | "from": "2.0.12",
116 | "to": "2.0.18"
117 | },
118 | {
119 | "version": "2.0.17.0.1",
120 | "from": "2.0.12",
121 | "to": "2.0.18"
122 | },
123 | {
124 | "version": "2.0.17.1.0",
125 | "from": "2.0.12",
126 | "to": "2.0.18"
127 | },
128 | {
129 | "version": "2.0.17.1.1",
130 | "from": "2.0.12",
131 | "to": "2.0.18",
132 | "changelog": ["Fix minor bugs related to map coordinates.","Show linked shared events on source page."]
133 | },
134 | {
135 | "version": "2.0.19.0.0",
136 | "from": "2.0.12",
137 | "to": "2.0.20"
138 | },
139 | {
140 | "version": "2.0.19.0.3",
141 | "from": "2.0.12",
142 | "to": "2.0.20"
143 | },
144 | {
145 | "version": "2.0.19.0.5",
146 | "from": "2.0.12",
147 | "to": "2.0.20",
148 | "changelog": ["Handle hierarchical places when searching for shared places."]
149 | },
150 | {
151 | "version": "2.0.19.1.0",
152 | "from": "2.0.12",
153 | "to": "2.0.20"
154 | },
155 | {
156 | "version": "2.0.19.2.0",
157 | "from": "2.0.12",
158 | "to": "2.0.20"
159 | },
160 | {
161 | "version": "2.0.22.0.0",
162 | "from": "2.0.12",
163 | "to": "2.0.23"
164 | },
165 | {
166 | "version": "2.0.23+2.1.0-beta.2.0.0",
167 | "from": "2.0.12",
168 | "to": "2.1.0"
169 | },
170 | {
171 | "version": "2.0.23+2.1.0-beta.2.0.1",
172 | "from": "2.0.12",
173 | "to": "2.1.0"
174 | },
175 | {
176 | "version": "2.0.23+2.1.0-beta.2.0.2",
177 | "from": "2.0.12",
178 | "to": "2.1.0"
179 | },
180 | {
181 | "version": "2.0.23+2.1.0-beta.2.1.0",
182 | "from": "2.0.12",
183 | "to": "2.1.0"
184 | },
185 | {
186 | "version": "2.0.23+2.1.0-beta.2.2.0",
187 | "from": "2.0.12",
188 | "to": "2.1.0"
189 | },
190 | {
191 | "version": "2.0.23+2.1.0-beta.2.3.0",
192 | "from": "2.0.12",
193 | "to": "2.1.0",
194 | "changelog": ["Preview of place history functionality (webtrees 2.1 branch only)."]
195 | },
196 | {
197 | "version": "2.1.0.0.0",
198 | "from": "2.0.12",
199 | "to": "2.1.1"
200 | },
201 | {
202 | "version": "2.1.0.1.0",
203 | "from": "2.0.12",
204 | "to": "2.1.1"
205 | },
206 | {
207 | "version": "2.1.1.0.0",
208 | "from": "2.0.12",
209 | "to": "2.1.2",
210 | "changelog": ["Place history functionality."]
211 | },
212 | {
213 | "version": "2.1.2.0.0",
214 | "from": "2.0.12",
215 | "to": "2.1.3"
216 | },
217 | {
218 | "version": "2.1.2.1.0",
219 | "from": "2.0.12",
220 | "to": "2.1.3",
221 | "changelog": ["Bugfixes (create shared place functionality)."]
222 | },
223 | {
224 | "version": "2.1.2.1.1",
225 | "from": "2.0.12",
226 | "to": "2.1.3"
227 | },
228 | {
229 | "version": "2.1.4.0.0",
230 | "from": "2.1.4",
231 | "to": "2.1.6"
232 | },
233 | {
234 | "version": "2.1.4.0.2",
235 | "from": "2.1.4",
236 | "to": "2.1.6"
237 | },
238 | {
239 | "version": "2.1.4.1.1",
240 | "from": "2.1.4",
241 | "to": "2.1.6"
242 | },
243 | {
244 | "version": "2.1.5.0.0",
245 | "from": "2.1.4",
246 | "to": "2.1.6"
247 | },
248 | {
249 | "version": "2.1.6.0.0",
250 | "from": "2.1.4",
251 | "to": "2.1.7"
252 | },
253 | {
254 | "version": "2.1.6.1.0",
255 | "from": "2.1.4",
256 | "to": "2.1.7",
257 | "changelog": ["Layout fix."]
258 | },
259 | {
260 | "version": "2.1.6.1.1",
261 | "from": "2.1.4",
262 | "to": "2.1.7"
263 | },
264 | {
265 | "version": "2.1.7.0.0",
266 | "from": "2.1.4",
267 | "to": "2.1.8"
268 | },
269 | {
270 | "version": "2.1.7.0.2",
271 | "from": "2.1.4",
272 | "to": "2.1.8"
273 | },
274 | {
275 | "version": "2.1.7.1.0",
276 | "from": "2.1.4",
277 | "to": "2.1.8"
278 | },
279 | {
280 | "version": "2.1.7.1.1",
281 | "from": "2.1.4",
282 | "to": "2.1.8",
283 | "changelog": ["Small bugfix."]
284 | },
285 | {
286 | "version": "2.1.8.0.0",
287 | "from": "2.1.8",
288 | "to": "2.1.14"
289 | },
290 | {
291 | "version": "2.1.8.0.2",
292 | "from": "2.1.8",
293 | "to": "2.1.14"
294 | },
295 | {
296 | "version": "2.1.9.0.0",
297 | "from": "2.1.8",
298 | "to": "2.1.14"
299 | },
300 | {
301 | "version": "2.1.9.1.0",
302 | "from": "2.1.8",
303 | "to": "2.1.14"
304 | },
305 | {
306 | "version": "2.1.9.1.0",
307 | "from": "2.1.8",
308 | "to": "2.1.14"
309 | },
310 | {
311 | "version": "2.1.9.9.9",
312 | "from": "2.1.10",
313 | "to": "2.1.14"
314 | },
315 | {
316 | "version": "2.1.10.0.0",
317 | "from": "2.1.10",
318 | "to": "2.1.14"
319 | },
320 | {
321 | "version": "2.1.13.0.0",
322 | "from": "2.1.10",
323 | "to": "2.1.14"
324 | },
325 | {
326 | "version": "2.1.15.0.0",
327 | "from": "2.1.10",
328 | "to": "2.1.17"
329 | },
330 | {
331 | "version": "2.1.15.0.2",
332 | "from": "2.1.10",
333 | "to": "2.1.17",
334 | "changelog": ["Bugfix (use Shared Places without Personal Facts module)."]
335 | },
336 | {
337 | "version": "2.1.15.0.3",
338 | "from": "2.1.10",
339 | "to": "2.1.17"
340 | },
341 | {
342 | "version": "2.1.16.0.0",
343 | "from": "2.1.10",
344 | "to": "2.1.17"
345 | },
346 | {
347 | "version": "2.1.16.1.0",
348 | "from": "2.1.10",
349 | "to": "2.1.17"
350 | },
351 | {
352 | "version": "2.1.16.1.3",
353 | "from": "2.1.10",
354 | "to": "2.1.17"
355 | },
356 | {
357 | "version": "2.1.16.1.5",
358 | "from": "2.1.10",
359 | "to": "2.1.17"
360 | },
361 | {
362 | "version": "2.1.16.1.6",
363 | "from": "2.1.10",
364 | "to": "2.1.17"
365 | },
366 | {
367 | "version": "2.1.16.2.0",
368 | "from": "2.1.10",
369 | "to": "2.1.17"
370 | },
371 | {
372 | "version": "2.1.17.0.0",
373 | "from": "2.1.17",
374 | "to": "2.1.18"
375 | },
376 | {
377 | "version": "2.1.17.1.0",
378 | "from": "2.1.17",
379 | "to": "2.1.18"
380 | },
381 | {
382 | "version": "2.1.18.0.0",
383 | "from": "2.1.17",
384 | "to": "2.1.21"
385 | },
386 | {
387 | "version": "2.1.18.0.1",
388 | "from": "2.1.17",
389 | "to": "2.1.21"
390 | },
391 | {
392 | "version": "2.1.18.1.0",
393 | "from": "2.1.17",
394 | "to": "2.1.21"
395 | },
396 | {
397 | "version": "2.1.18.2.0",
398 | "from": "2.1.17",
399 | "to": "2.1.21"
400 | },
401 | {
402 | "version": "2.1.18.2.2",
403 | "from": "2.1.17",
404 | "to": "2.1.21"
405 | },
406 | {
407 | "version": "2.1.19.0.0",
408 | "from": "2.1.17",
409 | "to": "2.1.21"
410 | },
411 | {
412 | "version": "2.1.20.0.0",
413 | "from": "2.1.17",
414 | "to": "2.1.21"
415 | },
416 | {
417 | "version": "2.1.20.1.0",
418 | "from": "2.1.17",
419 | "to": "2.1.21"
420 | },
421 | {
422 | "version": "2.1.20.2.0",
423 | "from": "2.1.17",
424 | "to": "2.1.21"
425 | },
426 | {
427 | "version": "2.2.0.0.0",
428 | "from": "2.1.17",
429 | "to": "2.2.1"
430 | },
431 | {
432 | "version": "2.2.1.0.0",
433 | "from": "2.1.17",
434 | "to": "2.2.2"
435 | },
436 | {
437 | "version": "2.2.1.1.0",
438 | "from": "2.1.17",
439 | "to": "2.2.2"
440 | },
441 | {
442 | "version": "2.2.1.2.0",
443 | "from": "2.2.1",
444 | "to": "2.2.2"
445 | },
446 | {
447 | "version": "2.2.1.3.0",
448 | "from": "2.2.1",
449 | "to": "2.2.2"
450 | },
451 | {
452 | "version": "2.2.1.4.0",
453 | "from": "2.2.1",
454 | "to": "2.2.2"
455 | },
456 | {
457 | "version": "2.2.1.5.0",
458 | "from": "2.2.1",
459 | "to": "2.2.2"
460 | },
461 | {
462 | "version": "2.2.2.0.0",
463 | "from": "2.2.1",
464 | "to": "2.2.4"
465 | },
466 | {
467 | "version": "2.2.3.0.0",
468 | "from": "2.2.1",
469 | "to": "2.2.4"
470 | },
471 | {
472 | "version": "2.2.4.0.0",
473 | "from": "2.2.1",
474 | "to": "2.2.5"
475 | },
476 | {
477 | "version": "2.2.4.1.0",
478 | "from": "2.2.1",
479 | "to": "2.2.5"
480 | }
481 | ]
482 |
--------------------------------------------------------------------------------
/patchedWebtrees/Http/RequestHandlers/CreateSharedPlaceAction.php:
--------------------------------------------------------------------------------
1 | tree();
35 |
36 | $params = (array) $request->getParsedBody();
37 | $useHierarchy = (bool) ($params['useHierarchy'] ?? false);
38 | $name = $params['shared-place-name'];
39 | $govId = $params['shared-place-govId'] ?? '';
40 |
41 | // Fix whitespace
42 | $name = trim(preg_replace('/\s+/', ' ', $name));
43 |
44 | if ($useHierarchy) {
45 | $ref = $this->createIfRequired($name, $govId, $tree);
46 | $record = $ref->record();
47 |
48 | if ($ref->created() === 0) {
49 | return response([
50 | 'html' => view('modals/record-created', [
51 | 'title' => I18N::translate('The shared place %s already exists.', $record->fullName()),
52 | 'name' => $record->fullName(),
53 | 'url' => $record->url(),
54 | ]),
55 | ], 409);
56 | } else {
57 | $html = '';
58 | if ($ref->created() === 2) {
59 | $html = ' ' . I18N::translate(' (Note: A higher-level shared place has also been created)');
60 | } else if ($ref->created() > 2) {
61 | $html = ' ' . I18N::translate(' (Note: %s higher-level shared places have also been created)', $ref->created() - 1);
62 | }
63 |
64 | // value and text and title are for autocomplete
65 | // html is for interactive modals
66 | return response([
67 | 'value' => '@' . $record->xref() . '@',
68 | 'text' => view('selects/location', [
69 | 'location' => $record,
70 | ]),
71 | //cf TomSelectSharedPlace, in this case same as text!
72 | 'title' => view('selects/location', [
73 | 'location' => $record,
74 | ]),
75 | 'html' => view('modals/record-created', [
76 | 'title' => I18N::translate('The shared place %s has been created.', $record->fullName()),
77 | 'name' => $record->fullName() . $html,
78 | 'url' => $record->url(),
79 | ]),
80 | ]);
81 | }
82 | }
83 |
84 | //else (no hierarchy)
85 |
86 | $privacy_restriction = Requests::getString($request, 'privacy-restriction');
87 | $edit_restriction = Requests::getString($request, 'edit-restriction');
88 |
89 | $gedcom = "0 @@ _LOC\n1 NAME " . $name;
90 |
91 | if ($govId != '') {
92 | $gedcom .= "\n1 _GOV " . $govId;
93 | }
94 |
95 | if (in_array($privacy_restriction, [
96 | 'none',
97 | 'privacy',
98 | 'confidential',
99 | ])) {
100 | $gedcom .= "\n1 RESN " . $privacy_restriction;
101 | }
102 |
103 | if (in_array($edit_restriction, ['locked'])) {
104 | $gedcom .= "\n1 RESN " . $edit_restriction;
105 | }
106 |
107 | $record = $tree->createRecord($gedcom); //returns GedcomRecord
108 | $record = Registry::locationFactory()->make($record->xref(), $tree); //we need Location for proper names!
109 | //FlashMessages::addMessage(I18N::translate('The shared place %s has been created.', $name), 'info');
110 | // id and text are for select2 / autocomplete
111 | // html is for interactive modals
112 | return response([
113 | 'id' => $record->xref(),
114 | 'text' => view('selects/location', [
115 | 'location' => $record,
116 | ]),
117 | //cf TomSelectSharedPlace, in this case same as text!
118 | 'title' => view('selects/location', [
119 | 'location' => $record,
120 | ]),
121 | 'html' => view('modals/record-created', [
122 | 'title' => I18N::translate('The shared place %s has been created.', $name),
123 | 'name' => $record->fullName(),
124 | 'url' => $record->url(),
125 | ]),
126 | ]);
127 | }
128 |
129 | public function createIfRequired(
130 | string $placeGedcomName,
131 | string $govId,
132 | Tree $tree,
133 | bool $simulate = false,
134 | ?SharedPlacesModule $enhanceWithGlobalData = null,
135 | bool $onlyIfGlobalDataAvailable = false): ?SharedPlaceRef {
136 |
137 | $parts = SharedPlace::placeNameParts($placeGedcomName);
138 | $tail = SharedPlace::placeNamePartsTail($parts);
139 | $head = reset($parts);
140 |
141 | //hacky - should we even support this here?
142 | if ($enhanceWithGlobalData !== null) {
143 | $useHierarchy = boolval($enhanceWithGlobalData->getPreference('USE_HIERARCHY', '1'));
144 |
145 | if (!$useHierarchy) {
146 | $head = $placeGedcomName;
147 | $tail = '';
148 | }
149 | }
150 |
151 | //if the place exists (with hierarchy), just return
152 | /* @var $searchService SearchServiceExt */
153 | $searchService = \Vesta\VestaUtils::get(SearchServiceExt::class);
154 | $sharedPlace = $searchService->searchLocationsInPlace(new Place($placeGedcomName, $tree))->first();
155 | if ($sharedPlace !== null) {
156 | return new SharedPlaceRef($sharedPlace, true, 0, null);
157 | }
158 |
159 | //otherwise create (including missing parents)
160 |
161 | $gedcom = "0 @@ _LOC\n1 NAME " . $head;
162 |
163 | $enhancedWithGlobalData = false;
164 |
165 | if ($govId != '') {
166 | $gedcom .= "\n1 _GOV " . $govId;
167 | } else if ($enhanceWithGlobalData !== null) {
168 | $plac2GovSupporters = $enhanceWithGlobalData->getPlac2GovSupporters($tree);
169 |
170 | if (sizeof($plac2GovSupporters) > 0) {
171 | foreach ($plac2GovSupporters as $plac2GovSupporter) {
172 | $gov = $plac2GovSupporter->plac2gov(PlaceStructure::fromName($placeGedcomName, $tree));
173 | if ($gov !== null) {
174 | $gedcom .= "\n1 _GOV " . $gov->getId();
175 | $enhancedWithGlobalData = true;
176 | break;
177 | }
178 | }
179 | }
180 | }
181 |
182 | if ($enhanceWithGlobalData !== null) {
183 | $ll = $enhanceWithGlobalData->getLatLon($placeGedcomName);
184 |
185 | if ($ll !== null) {
186 | $map_lati = ($ll[0] < 0) ? "S" . str_replace('-', '', (string) $ll[0]) : "N" . $ll[0];
187 | $map_long = ($ll[1] < 0) ? "W" . str_replace('-', '', (string) $ll[1]) : "E" . $ll[1];
188 | $gedcom .= "\n1 MAP\n2 LATI " . $map_lati . "\n2 LONG " . $map_long;
189 | $enhancedWithGlobalData = true;
190 | }
191 | }
192 |
193 | if ($onlyIfGlobalDataAvailable && !$enhancedWithGlobalData) {
194 | return null;
195 | }
196 |
197 | /////////////////////////////////////////////////////////////////////////
198 | //start to actually change something!
199 |
200 | $ref = null;
201 | if ($tail !== '') {
202 | //missing parents have to be created regardless of their own $onlyIfGlobalDataAvailable!
203 | $ref = $this->createIfRequired($tail, '', $tree, $simulate, $enhanceWithGlobalData);
204 | }
205 |
206 | if ($ref !== null) {
207 | $gedcom .= "\n1 _LOC @" . $ref->record()->xref() . "@";
208 | $gedcom .= "\n2 TYPE POLI";
209 | }
210 |
211 | if (!$simulate) {
212 | $record = $tree->createRecord($gedcom); //returns GedcomRecord
213 | }
214 | $newXref = 'NX_' . CreateSharedPlaceAction::generateRandomString(16);
215 | if (!$simulate) {
216 | $newXref = $record->xref();
217 | }
218 |
219 | //we need Location for proper names!
220 | //and we must check() in order to update place links
221 | /** @var SharedPlace $record */
222 |
223 | $record = Registry::locationFactory()->make($newXref, $tree, $gedcom);
224 | $record->check();
225 |
226 | $count = 1;
227 | if ($ref !== null) {
228 | $count += $ref->created();
229 | }
230 | return new SharedPlaceRef($record, false, $count, $ref);
231 | }
232 |
233 | //Uuid::uuid4() is to long for XREF (max length 20)
234 | //https://stackoverflow.com/questions/4356289/php-random-string-generator
235 | public static function generateRandomString($length = 10) {
236 | $characters = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
237 | $charactersLength = strlen($characters);
238 | $randomString = '';
239 | for ($i = 0; $i < $length; $i++) {
240 | $randomString .= $characters[rand(0, $charactersLength - 1)];
241 | }
242 | return $randomString;
243 | }
244 |
245 | }
246 |
--------------------------------------------------------------------------------
/patchedWebtrees/PlaceViaSharedPlace.php:
--------------------------------------------------------------------------------
1 | actual = $actual;
53 | $this->asAdditionalParticipant = $asAdditionalParticipant;
54 | $this->urls = $urls;
55 | $this->module = $module;
56 | $this->sharedPlaces = $sharedPlaces;
57 | }
58 |
59 | //Speedup
60 | //more efficient than $this->actual->url() when calling for lots of places
61 | //this should be in webtrees, e.g. via caching result of 'findByComponent' or even 'Auth::accessLevel'
62 | public function url(): string {
63 | return $this->urls->url($this->actual);
64 | }
65 |
66 | public function gedcomName(): string {
67 | return $this->actual->gedcomName();
68 | }
69 |
70 | public function placeName(): string {
71 | $uniqueSharedPlaces = boolval($this->module->getPreference('UNIQUE_SP_IN_HIERARCHY', '0'));
72 |
73 | if ($uniqueSharedPlaces && !$this->asAdditionalParticipant) {
74 | //by design only current names (exclude historical names!)
75 | $place_name = $this->sharedPlaces
76 | ->flatMap(function (SharedPlace $sharedPlace): array {
77 | return $sharedPlace->namesNNAt(null);
78 | })
79 | ->reduce(function ($carry, $item): string {
80 | return ($carry === "")?$item:$carry . " | " . $item;
81 | }, "");
82 |
83 | return '
' . e($place_name) . '';
84 | }
85 |
86 | return $this->actual->placeName();
87 | }
88 |
89 | //cf DefaultPlaceWithinHierarchy, but map to SharedPlace/PlaceViaSharedPlace directly from query
90 | public function getChildPlacesCacheIds(
91 | Place $place): Collection {
92 |
93 | $self = $this;
94 |
95 | if ($place->gedcomName() !== '') {
96 | $parent_text = Gedcom::PLACE_SEPARATOR . $place->gedcomName();
97 | } else {
98 | $parent_text = '';
99 | }
100 |
101 | $tree = $place->tree();
102 |
103 | $place2sharedPlaceMap = DB::table('places')
104 | ->where('p_file', '=', $tree->id())
105 | ->where('p_parent_id', '=', $place->id())
106 | ->join('placelinks', static function (JoinClause $join): void {
107 | $join
108 | ->on('pl_file', '=', 'p_file')
109 | ->on('pl_p_id', '=', 'p_id');
110 | })
111 | ->join('other', static function (JoinClause $join): void {
112 | $join
113 | ->on('o_file', '=', 'pl_file')
114 | ->on('o_id', '=', 'pl_gid');
115 | })
116 | ->where('o_type', '=', '_LOC')
117 | ->get()
118 | ->map(function (stdClass $row) use ($parent_text, $tree): array {
119 | $place = new Place($row->p_place . $parent_text, $tree);
120 | $id = $row->p_id;
121 | Registry::cache()->array()->remember('place-' . $place->gedcomName(), function () use ($id): int {return $id;});
122 |
123 | $sharedPlace = Registry::locationFactory()->mapper($tree)($row);
124 | return ["actual" => $place, "record" => $sharedPlace];
125 | })
126 | //must filter as in SearchServiceExt::searchLocationsInPlace
127 | ->filter(function ($item): bool {
128 | //include only if name matches!
129 | $names = new Collection($item["record"]->namesAsPlacesAt(GedcomDateInterval::createEmpty()));
130 | return $names->has($item["actual"]->id());
131 | });
132 |
133 | if ($place2sharedPlaceMap->isEmpty()) {
134 | return new Collection();
135 | }
136 |
137 | $uniqueSharedPlaces = boolval($this->module->getPreference('UNIQUE_SP_IN_HIERARCHY', '0'));
138 |
139 | //if $asAdditionalParticipant, we just need the additional data (map coordinates)
140 | //and do not evaluate $uniqueSharedPlaces
141 | if ($uniqueSharedPlaces && !$this->asAdditionalParticipant) {
142 | $place2sharedPlaceMap = $place2sharedPlaceMap
143 | ->mapToGroups(static function ($item): array {
144 | /** @var SharedPlace $sharedPlace */
145 | $sharedPlace = $item["record"];
146 | return [$sharedPlace->xref() => $item];
147 | })
148 | ->map(static function (Collection $groupedItems): array {
149 | $first = $groupedItems->first();
150 | /** @var SharedPlace $sharedPlace */
151 | $sharedPlace = $first["record"];
152 |
153 | //which place to use? follow order from shared place
154 | $place = null;
155 | foreach ($sharedPlace->namesAsPlacesAt(GedcomDateInterval::createEmpty()) as $placeViaSharedPlace) {
156 | foreach ($groupedItems as $groupedItem) {
157 | if ($placeViaSharedPlace->id() === $groupedItem["actual"]->id()) {
158 | $place = $groupedItem["actual"];
159 | break 2;
160 | }
161 | }
162 | }
163 |
164 | if ($place === null) {
165 | throw new Exception("unexpected null place!");
166 | }
167 | return ["actual" => $place, "record" => $sharedPlace];
168 | });
169 | }
170 |
171 | $childPlaces = $place2sharedPlaceMap
172 | ->mapToGroups(static function ($item): array {
173 | $place = $item["actual"];
174 | return [$place->id() => $item];
175 | })
176 | ->map(static function (Collection $groupedItems) use ($self): PlaceViaSharedPlace {
177 | $first = $groupedItems->first();
178 | $sharedPlaces = $groupedItems
179 | ->map(static function (array $inner): SharedPlace {
180 | return $inner["record"];
181 | })
182 | ->unique(function (SharedPlace $sharedPlace): string {
183 | return $sharedPlace->xref();
184 | });
185 |
186 | return new PlaceViaSharedPlace(
187 | $first["actual"],
188 | $self->asAdditionalParticipant,
189 | $self->urls,
190 | $sharedPlaces,
191 | $self->module);
192 | })
193 | ->sort(static function (PlaceViaSharedPlace $x, PlaceViaSharedPlace $y): int {
194 | return strtolower($x->gedcomName()) <=> strtolower($y->gedcomName());
195 | })
196 | ->mapWithKeys(static function (PlaceViaSharedPlace $place): array {
197 | return [$place->id() => $place];
198 | });
199 |
200 | return $childPlaces;
201 | }
202 |
203 | public function getChildPlaces(): array {
204 | $ret = $this
205 | ->getChildPlacesCacheIds($this->actual)
206 | ->toArray();
207 |
208 | return $ret;
209 | }
210 |
211 | public function id(): int {
212 | return $this->actual->id();
213 | }
214 |
215 | public function tree(): Tree {
216 | return $this->actual->tree();
217 | }
218 |
219 | public function fullName(bool $link = false): string {
220 | return $this->actual->fullName($link);
221 | }
222 |
223 | public function searchIndividualsInPlace(): Collection {
224 | return SharedPlace::linkedIndividualsRecords($this->sharedPlaces);
225 | }
226 |
227 | public function countIndividualsInPlace(): int {
228 | return SharedPlace::linkedIndividualsCount($this->sharedPlaces);
229 | }
230 |
231 | public function searchFamiliesInPlace(): Collection {
232 | return SharedPlace::linkedFamiliesRecords($this->sharedPlaces);
233 | }
234 |
235 | public function countFamiliesInPlace(): int {
236 | return SharedPlace::linkedFamiliesCount($this->sharedPlaces);
237 | }
238 |
239 | protected function initLatLon(): ?MapCoordinates {
240 | $useIndirectLinks = boolval($this->module->getPreference('INDIRECT_LINKS', '1'));
241 |
242 | if (!$useIndirectLinks) {
243 | //check shared places directly, they won't be checked via plac2map
244 | foreach ($this->sharedPlaces as $sharedPlace) {
245 | /* @var $sharedPlace SharedPlace */
246 |
247 | $locReference = new LocReference($sharedPlace->xref(), $sharedPlace->tree(), new Trace(''));
248 | $mapCoordinates = FunctionsPlaceUtils::loc2map($this->module, $locReference);
249 | if ($mapCoordinates !== null) {
250 | return $mapCoordinates;
251 | }
252 | }
253 | }
254 |
255 | $ps = $this->placeStructure();
256 | if ($ps === null) {
257 | return null;
258 | }
259 | return FunctionsPlaceUtils::plac2map($this->module, $ps, false);
260 | }
261 |
262 | public function getLatLon(): ?MapCoordinates {
263 | if (!$this->latLonInitialized) {
264 | $this->latLon = $this->initLatLon();
265 | $this->latLonInitialized = true;
266 | }
267 |
268 | return $this->latLon;
269 | }
270 |
271 | public function latitude(): ?float {
272 | //we don't go up the hierarchy here - there may be more than one parent!
273 |
274 | $lati = null;
275 | if ($this->getLatLon() !== null) {
276 | $lati = $this->getLatLon()->getLati();
277 | }
278 | if ($lati === null) {
279 | return null;
280 | }
281 |
282 | $gedcom_service = new GedcomService();
283 | return $gedcom_service->readLatitude($lati);
284 | }
285 |
286 | public function longitude(): ?float {
287 | //we don't go up the hierarchy here - there may be more than one parent!
288 |
289 | $long = null;
290 | if ($this->getLatLon() !== null) {
291 | $long = $this->getLatLon()->getLong();
292 | }
293 | if ($long === null) {
294 | return null;
295 | }
296 |
297 | $gedcom_service = new GedcomService();
298 | return $gedcom_service->readLongitude($long);
299 | }
300 |
301 | public function icon(): string {
302 | return '';
303 | }
304 |
305 | public function boundingRectangleWithChildren(array $children): array
306 | {
307 | /*
308 | if (top-level) {
309 | //why doesn't original impl calculate bounding rectangle for world? Too expensive?
310 | return [[-180.0, -90.0], [180.0, 90.0]];
311 | }
312 | */
313 |
314 | $latitudes = [];
315 | $longitudes = [];
316 |
317 | if ($this->latitude() !== null) {
318 | $latitudes[] = $this->latitude();
319 | }
320 | if ($this->longitude() !== null) {
321 | $longitudes[] = $this->longitude();
322 | }
323 |
324 | foreach ($children as $child) {
325 | if ($child->latitude() !== null) {
326 | $latitudes[] = $child->latitude();
327 | }
328 | if ($child->longitude() !== null) {
329 | $longitudes[] = $child->longitude();
330 | }
331 | }
332 |
333 | if ((count($latitudes) === 0) || (count($longitudes) === 0)) {
334 | return [[-180.0, -90.0], [180.0, 90.0]];
335 | }
336 |
337 | $latiMin = (new Collection($latitudes))->min();
338 | $longMin = (new Collection($longitudes))->min();
339 | $latiMax = (new Collection($latitudes))->max();
340 | $longMax = (new Collection($longitudes))->max();
341 |
342 | //never zoom in too far (in particular if there is only one place, but also if the places are close together)
343 | $latiSpread = $latiMax - $latiMin;
344 | if ($latiSpread < 1) {
345 | $latiMin -= (1 - $latiSpread)/2;
346 | $latiMax += (1 - $latiSpread)/2;
347 | }
348 |
349 | $longSpread = $longMax - $longMin;
350 | if ($longSpread < 1) {
351 | $longMin -= (1 - $longSpread)/2;
352 | $longMax += (1 - $longSpread)/2;
353 | }
354 |
355 | return [[$latiMin, $longMin], [$latiMax, $longMax]];
356 | }
357 |
358 | public function placeStructure(): ?PlaceStructure {
359 | return PlaceStructure::fromPlace($this->actual);
360 | }
361 |
362 | public function additionalLinksHtmlBeforeName(): string {
363 | $html = '';
364 | if ($this->module !== null) {
365 | foreach ($this->sharedPlaces as $sharedPlace) {
366 | $html .= $this->module->getLinkForSharedPlace($sharedPlace);
367 | }
368 | }
369 |
370 | return $html;
371 | }
372 |
373 | public function links(): Collection {
374 | return $this->urls->links($this->actual);
375 | }
376 |
377 | public function parent(): PlaceWithinHierarchy {
378 | return $this->module->findPlace($this->actual->parent()->id(), $this->actual->tree(), $this->urls);
379 | }
380 | }
381 |
--------------------------------------------------------------------------------
/patchedWebtrees/Services/SearchServiceExt.php:
--------------------------------------------------------------------------------
1 | tree_service = $tree_service;
38 | }
39 |
40 | public function searchLocationsInPlace(Place $place): Collection {
41 | //it may seem more efficient to filter to roots via LocGraph,
42 | //but there are edge cases where that isn't correct (if a shared places maps to "A, B" as well as "B")
43 |
44 | return $this->searchLocationHierarchiesInPlace($place)
45 | ->filter(function (SharedPlace $sharedPlace) use ($place): bool {
46 | //include only if name matches!
47 | $names = new Collection($sharedPlace->namesAsPlacesAt(GedcomDateInterval::createEmpty()));
48 | return $names->has($place->id());
49 | });
50 | }
51 |
52 | public function searchLocationsInPlaces(Tree $tree, Collection $places): Collection {
53 | //it may seem more efficient to filter to roots via LocGraph,
54 | //but there are edge cases where that isn't correct (if a shared places maps to "A, B" as well as "B")
55 |
56 | return $this->searchLocationHierarchiesInPlaces($tree, $places)
57 | ->filter(function (SharedPlace $sharedPlace) use ($places): bool {
58 | //include only if name matches!
59 | $names = new Collection($sharedPlace->namesAsPlacesAt(GedcomDateInterval::createEmpty()));
60 |
61 | $anyHas = $places->first(static function ($place) use ($names) {
62 | return $names->has($place->id());
63 | });
64 | return ($anyHas != null);
65 | });
66 | }
67 |
68 | //[2021/03] now that placelinks includes all child LOCs as well (placelinks requires these to prevent orphaning),
69 | //this function returns more than usually intended: cf searchLocationsInPlace
70 | public function searchLocationHierarchiesInPlace(Place $place): Collection {
71 | return DB::table('other')
72 | ->join('placelinks', static function (JoinClause $query) {
73 | $query
74 | ->on('other.o_file', '=', 'placelinks.pl_file')
75 | ->on('other.o_id', '=', 'placelinks.pl_gid');
76 | })
77 | ->where('o_type', '=', '_LOC')
78 | ->where('o_file', '=', $place->tree()->id())
79 | ->where('pl_p_id', '=', $place->id())
80 | ->select(['other.*'])
81 | ->get()
82 | //->each($this->rowLimiter()) //unlikely to be relevant anyway
83 | ->map($this->locationRowMapper())
84 | ->filter(GedcomRecord::accessFilter());
85 | }
86 |
87 | public function searchLocationHierarchiesInPlaces(Tree $tree, Collection $places): Collection {
88 | return DB::table('other')
89 | ->join('placelinks', static function (JoinClause $query) {
90 | $query
91 | ->on('other.o_file', '=', 'placelinks.pl_file')
92 | ->on('other.o_id', '=', 'placelinks.pl_gid');
93 | })
94 | ->where('o_type', '=', '_LOC')
95 | ->where('o_file', '=', $tree->id())
96 | ->whereIn('pl_p_id', $places->map(static function (Place $place): int {
97 | return $place->id();
98 | })->all())
99 | ->select(['other.*'])
100 | ->get()
101 | //->each($this->rowLimiter()) //unlikely to be relevant anyway
102 | ->map($this->locationRowMapper())
103 | ->filter(GedcomRecord::accessFilter());
104 | }
105 |
106 | /**
107 | * Search for shared places.
108 | *
109 | * @param Tree[] $trees
110 | * @param string[] $search
111 | * @param int $offset
112 | * @param int $limit
113 | *
114 | * @return Collection|Location[]
115 | */
116 | public function searchLocations(array $trees, array $search, int $offset = 0, int $limit = PHP_INT_MAX): Collection {
117 | $query = DB::table('other')
118 | ->where('o_type', '=', '_LOC');
119 |
120 | $this->whereTrees($query, 'o_file', $trees);
121 | $this->whereSearch($query, 'o_gedcom', $search);
122 |
123 | return $this->paginateQuery(
124 | $query,
125 | $this->locationRowMapper(),
126 | GedcomRecord::accessFilter(),
127 | $offset,
128 | $limit);
129 | }
130 |
131 | /**
132 | * Search for shared places.
133 | *
134 | * @param Tree[] $trees
135 | * @param string[] $search
136 | * @param int $offset
137 | * @param int $limit
138 | *
139 | * @return Collection|Location[]
140 | */
141 | public function searchLocationsEOL(array $trees, array $search, int $offset = 0, int $limit = PHP_INT_MAX): Collection {
142 | $query = DB::table('other')
143 | ->where('o_type', '=', '_LOC');
144 |
145 | $this->whereTrees($query, 'o_file', $trees);
146 | $this->whereSearchEOL($query, 'o_gedcom', $search);
147 |
148 | return $this->paginateQuery($query, $this->locationRowMapper(), GedcomRecord::accessFilter(), $offset, $limit);
149 | }
150 |
151 | public function searchTopLevelLocations(array $trees, int $offset = 0, int $limit = PHP_INT_MAX): Collection {
152 | //not useful because a location may have links to parent locations for some dates
153 | //while being a top-level location at some other date
154 | /*
155 | $query = DB::table('other')
156 | ->leftJoin('link', static function (JoinClause $join): void {
157 | $join
158 | ->on('l_file', '=', 'o_file')
159 | ->on('l_from', '=', 'o_id')
160 | ->where('l_type', '=', '_LOC');
161 | })
162 | ->whereNull('l_from')
163 | ->where('o_type', '=', '_LOC');
164 |
165 | $this->whereTrees($query, 'o_file', $trees);
166 | */
167 |
168 | //a top-level location is a location linked to at least one top-level (i.e. parentless) place
169 | /* @var $query Builder */
170 | $query = DB::table('other')
171 | ->join('placelinks', static function (JoinClause $join): void {
172 | $join
173 | ->on('pl_file', '=', 'o_file')
174 | ->on('pl_gid', '=', 'o_id');
175 | })
176 | ->join('places', static function (JoinClause $join): void {
177 | $join
178 | ->on('p_id', '=', 'pl_p_id');
179 | })
180 | ->where('p_parent_id', '=', 0)
181 | ->where('o_type', '=', '_LOC');
182 |
183 | $this->whereTrees($query, 'o_file', $trees);
184 | $query->distinct();
185 | $query->select(['o_id', 'o_file', 'o_type', 'o_gedcom']); //must select explicitly, otherwise '*' which messes up the distinct
186 |
187 | return $this->paginateQuery($query, $this->locationRowMapper(), GedcomRecord::accessFilter(), $offset, $limit);
188 | }
189 |
190 | private function locationRowMapper(): Closure {
191 | return function (stdClass $row): Location {
192 | /*
193 | try {
194 | $tree = $this->tree_service->find((int) $row->o_file);
195 | } catch (\Exception $ex) {
196 | error_log("private tree? " . $row->o_file);
197 | error_log(print_r($this->tree_service->all(), true));
198 | throw new \Exception("private tree? " . $row->o_file);
199 | }
200 | */
201 | $tree = $this->tree_service->find((int) $row->o_file);
202 |
203 | return Registry::locationFactory()->mapper($tree)($row);
204 | };
205 | }
206 |
207 | /**
208 | * Paginate a search query.
209 | *
210 | * @param Builder $query Searches the database for the desired records.
211 | * @param Closure $row_mapper Converts a row from the query into a record.
212 | * @param Closure $row_filter
213 | * @param int $offset Skip this many rows.
214 | * @param int $limit Take this many rows.
215 | *
216 | * @return Collection
217 | */
218 | private function paginateQuery(Builder $query, Closure $row_mapper, Closure $row_filter, int $offset, int $limit): Collection {
219 | $collection = new Collection();
220 |
221 | foreach ($query->cursor() as $row) {
222 | $record = $row_mapper($row);
223 | // If the object has a method "canShow()", then use it to filter for privacy.
224 | if ($row_filter($record)) {
225 | if ($offset > 0) {
226 | $offset--;
227 | } else {
228 | if ($limit > 0) {
229 | $collection->push($record);
230 | }
231 |
232 | $limit--;
233 |
234 | if ($limit === 0) {
235 | break;
236 | }
237 | }
238 | }
239 | }
240 |
241 | return $collection;
242 | }
243 |
244 | /**
245 | * Apply search filters to a SQL query column. Apply collation rules to MySQL.
246 | *
247 | * @param Builder $query
248 | * @param Expression|string $field
249 | * @param string[] $search_terms
250 | */
251 | private function whereSearch(Builder $query, $field, array $search_terms): void {
252 | if ($field instanceof Expression) {
253 | $field = $field->getValue();
254 | }
255 |
256 | foreach ($search_terms as $search_term) {
257 | $query->where(new Expression($field), 'LIKE', '%' . addcslashes($search_term, '\\%_') . '%');
258 | }
259 | }
260 |
261 | private function whereSearchEOL(Builder $query, $field, array $search_terms): void {
262 | if ($field instanceof Expression) {
263 | $field = $field->getValue();
264 | }
265 |
266 | foreach ($search_terms as $search_term) {
267 | //issue #122
268 | //this doesn't nest the disjunction as intended!
269 | /*
270 | $query
271 | ->where(new Expression($field), 'LIKE', '%' . addcslashes($search_term . "\n", '\\%_') . '%') //EOL
272 | ->orWhere(new Expression($field), 'LIKE', '%' . addcslashes($search_term, '\\%_')); //EOL via end of entire entry
273 | */
274 | $query->where(static function (Builder $q) use ($field, $search_term): void {
275 | $q
276 | ->where(new Expression($field), 'LIKE', '%' . addcslashes($search_term . "\n", '\\%_') . '%') //EOL
277 | ->orWhere(new Expression($field), 'LIKE', '%' . addcslashes($search_term, '\\%_')); //EOL via end of entire entry
278 | });
279 | }
280 | }
281 |
282 | /**
283 | * @param Builder $query
284 | * @param string $tree_id_field
285 | * @param Tree[] $trees
286 | */
287 | private function whereTrees(Builder $query, string $tree_id_field, array $trees): void {
288 | $tree_ids = array_map(function (Tree $tree) {
289 | return $tree->id();
290 | }, $trees);
291 |
292 | $query->whereIn($tree_id_field, $tree_ids);
293 | }
294 |
295 | //same as main, but
296 | //a) handle search strings 'A, B' (main only handles 'A,B')
297 | //b) search first for 'startingWith'
298 | /**
299 | * Search for places.
300 | *
301 | * @param Tree $tree
302 | * @param string $search
303 | * @param int $offset
304 | * @param int $limit
305 | *
306 | * @return Collection
307 | */
308 | public function searchPlaces(
309 | Tree $tree,
310 | string $search,
311 | bool $startsWith = false,
312 | int $offset = 0,
313 | int $limit = PHP_INT_MAX): Collection {
314 |
315 | $query = DB::table('places AS p0')
316 | ->where('p0.p_file', '=', $tree->id())
317 | ->leftJoin('places AS p1', 'p1.p_id', '=', 'p0.p_parent_id')
318 | ->leftJoin('places AS p2', 'p2.p_id', '=', 'p1.p_parent_id')
319 | ->leftJoin('places AS p3', 'p3.p_id', '=', 'p2.p_parent_id')
320 | ->leftJoin('places AS p4', 'p4.p_id', '=', 'p3.p_parent_id')
321 | ->leftJoin('places AS p5', 'p5.p_id', '=', 'p4.p_parent_id')
322 | ->leftJoin('places AS p6', 'p6.p_id', '=', 'p5.p_parent_id')
323 | ->leftJoin('places AS p7', 'p7.p_id', '=', 'p6.p_parent_id')
324 | ->leftJoin('places AS p8', 'p8.p_id', '=', 'p7.p_parent_id')
325 | ->orderBy('p0.p_place')
326 | ->orderBy('p1.p_place')
327 | ->orderBy('p2.p_place')
328 | ->orderBy('p3.p_place')
329 | ->orderBy('p4.p_place')
330 | ->orderBy('p5.p_place')
331 | ->orderBy('p6.p_place')
332 | ->orderBy('p7.p_place')
333 | ->orderBy('p8.p_place')
334 | ->select([
335 | 'p0.p_place AS place0',
336 | 'p1.p_place AS place1',
337 | 'p2.p_place AS place2',
338 | 'p3.p_place AS place3',
339 | 'p4.p_place AS place4',
340 | 'p5.p_place AS place5',
341 | 'p6.p_place AS place6',
342 | 'p7.p_place AS place7',
343 | 'p8.p_place AS place8',
344 | ]);
345 |
346 | // Filter each level of the hierarchy.
347 | foreach (explode(',', $search, 9) as $level => $string) {
348 | $prefix = '';
349 | if ($startsWith) {
350 | $prefix = '%';
351 | }
352 | $query->where('p' . $level . '.p_place', 'LIKE', $prefix . addcslashes(trim($string), '\\%_') . '%');
353 | }
354 |
355 | $row_mapper = static function (stdClass $row) use ($tree): Place {
356 | $place = implode(', ', array_filter((array) $row));
357 |
358 | return new Place($place, $tree);
359 | };
360 |
361 | $filter = static function (): bool {
362 | return true;
363 | };
364 |
365 | return $this->paginateQuery($query, $row_mapper, $filter, $offset, $limit);
366 | }
367 |
368 | }
369 |
--------------------------------------------------------------------------------
/SharedPlacesModuleTrait.php:
--------------------------------------------------------------------------------
1 | '.CommonI18N::readme().'';
34 | $link2 = ''.CommonI18N::readmeLocationData().'';
35 |
36 | $description = array();
37 | //TODO add link to https://genealogy.net/GEDCOM/
38 | $description[] = /* I18N: Module Configuration */I18N::translate('A module supporting shared places as level 0 GEDCOM objects, on the basis of the GEDCOM-L Addendum to the GEDCOM 5.5.1 specification. Shared places may contain e.g. map coordinates, notes and media objects. The module displays this data for all matching places via the extended \'Facts and events\' tab. It may also be used to manage GOV ids, in combination with the Gov4Webtrees module.');
39 | /*$description[] =*/ /* I18N: Module Configuration *//*I18N::translate('Replaces the original \'Locations\' module.');*/
40 | $description[] =
41 | CommonI18N::requires2(CommonI18N::titleVestaCommon(), CommonI18N::titleVestaPersonalFacts());
42 | $description[] =
43 | CommonI18N::providesLocationData();
44 | $description[] = $link1 . '. ' . $link2 . '.';;
45 | return $description;
46 | }
47 |
48 | protected function createPrefs() {
49 | $generalSub = array();
50 | $generalSub[] = new ControlPanelSubsection(
51 | CommonI18N::displayedTitle(),
52 | array(
53 | /*new ControlPanelCheckbox(
54 | I18N::translate('Include the %1$s symbol in the module title', $this->getVestaSymbol()),
55 | null,
56 | 'VESTA',
57 | '1'),*/
58 | new ControlPanelCheckbox(
59 | CommonI18N::vestaSymbolInListTitle(),
60 | CommonI18N::vestaSymbolInTitle2(),
61 | 'VESTA_LIST',
62 | '1')));
63 |
64 | $link = ''.CommonI18N::readme().'';
65 |
66 | $generalSub[] = new ControlPanelSubsection(
67 | /* I18N: Module Configuration */I18N::translate('Shared place structure'),
68 | array(new ControlPanelCheckbox(
69 | /* I18N: Module Configuration */I18N::translate('Use hierarchical shared places'),
70 | /* I18N: Module Configuration */I18N::translate('If checked, relations between shared places are modelled via an explicit hierarchy, where shared places have XREFs to higher-level shared places, as described in the specification.') . ' ' .
71 | /* I18N: Module Configuration */I18N::translate('Note that this also affects the way shared places are created, and the way they are mapped to places.') . ' ' .
72 | /* I18N: Module Configuration */I18N::translate('In particular, hierarchical shared places do not have names with comma-separated name parts.') . ' ' .
73 | /* I18N: Module Configuration */I18N::translate('See %1$s for details.', $link) . ' ' .
74 | /* I18N: Module Configuration */I18N::translate('There is a data fix available which may be used to convert existing shared places.') . ' ' .
75 | /* I18N: Module Configuration */I18N::translate('When unchecked, the former approach is used, in which hierarchies are only hinted at by using shared place names with comma-separated name parts.') . ' ' .
76 | /* I18N: Module Configuration */I18N::translate('It is strongly recommended to switch to hierarchical shared places.'),
77 | 'USE_HIERARCHY',
78 | '1')));
79 |
80 | $generalSub[] = new ControlPanelSubsection(
81 | /* I18N: Module Configuration */I18N::translate('Linking of shared places to places'),
82 | array(
83 | new ControlPanelCheckbox(
84 | /* I18N: Module Configuration */I18N::translate('Additionally link shared places via name'),
85 | /* I18N: Module Configuration */I18N::translate('According to the GEDCOM-L Addendum, shared places are referenced via XREFs, just like shared notes etc. ') .
86 | /* I18N: Module Configuration */I18N::translate('It is now recommended to use XREFs, as this improves performance and flexibility. There is a data fix available which may be used to add XREFs. ') .
87 | /* I18N: Module Configuration */I18N::translate('However, you can still check this option and link shared places via the place name itself. In this case, links are established internally by searching for a shared place with any name matching case-insensitively.') . ' ' .
88 | /* I18N: Module Configuration */I18N::translate('If you are using hierarchical shared places, a place with the name "A, B, C" is mapped to a shared place "A" with a higher-level shared place that maps to "B, C".'),
89 | 'INDIRECT_LINKS',
90 | '0'),
91 | new ControlPanelRange(
92 | /* I18N: Module Configuration */I18N::translate('... and fall back to n parent levels'),
93 | /* I18N: Module Configuration */I18N::translate('When the preceding option is checked, and no matching shared place is found, fall back to n parent places (so that e.g. for n=2 a place "A, B, C" would also match shared places that match "B, C" and "C")'),
94 | 0,
95 | 5,
96 | 'INDIRECT_LINKS_PARENT_LEVELS',
97 | 0)));
98 |
99 | $factsSub = array();
100 |
101 | //TODO: make this configurable again?
102 | if (false) {
103 | $factsSub[] = new ControlPanelSubsection(
104 | /* I18N: Module Configuration */I18N::translate('All shared place facts'),
105 | array(
106 | ControlPanelFactRestriction::createWithFacts(
107 | SharedPlacesModuleTrait::getPicklistFactsLoc(),
108 | /* I18N: Module Configuration */I18N::translate('This is the list of GEDCOM facts that your users can add to shared places. You can modify this list by removing or adding fact names as necessary. Fact names that appear in this list must not also appear in the “Unique shared place facts” list.'),
109 | '_LOC_FACTS_ADD',
110 | 'NAME,_LOC:TYPE,NOTE,SHARED_NOTE,SOUR,_LOC:_LOC')));
111 | $factsSub[] = new ControlPanelSubsection(
112 | /* I18N: Module Configuration */I18N::translate('Unique shared place facts'),
113 | array(
114 | ControlPanelFactRestriction::createWithFacts(
115 | SharedPlacesModuleTrait::getPicklistFactsLoc(),
116 | /* I18N: Module Configuration */I18N::translate('This is the list of GEDCOM facts that your users can only add once to shared places. For example, if NAME is in this list, users will not be able to add more than one NAME record to a shared place. Fact names that appear in this list must not also appear in the “All shared place facts” list.'),
117 | '_LOC_FACTS_UNIQUE',
118 | 'MAP,_GOV')));
119 |
120 | //this is prepared for in the modal, but apparently was never used
121 | //really not that useful currently
122 | /*
123 | $factsSub[] = new ControlPanelSubsection(
124 | I18N::translate('Facts for new shared places'),
125 | array(
126 | ControlPanelFactRestriction::createWithFacts(
127 | SharedPlacesModuleTrait::getPicklistFactsLoc(true),
128 | I18N::translate('This is the list of GEDCOM facts that will be shown when adding a new shared place.'),
129 | '_LOC_FACTS_REQUIRED',
130 | '')));
131 | */
132 |
133 | $factsSub[] = new ControlPanelSubsection(
134 | /* I18N: Module Configuration */I18N::translate('Quick shared place facts'),
135 | array(
136 | ControlPanelFactRestriction::createWithFacts(
137 | SharedPlacesModuleTrait::getPicklistFactsLoc(),
138 | /* I18N: Module Configuration */I18N::translate('This is the list of GEDCOM facts that your users can add to shared places. You can modify this list by removing or adding fact names as necessary. Fact names that appear in this list must not also appear in the “Unique shared place facts” list. '),
139 | '_LOC_FACTS_QUICK',
140 | 'NAME,_LOC:_LOC,MAP,NOTE,SHARED_NOTE,_GOV')));
141 | }
142 |
143 | $factsAndEventsSub = array();
144 | $factsAndEventsSub[] = new ControlPanelSubsection(
145 | CommonI18N::displayedData(),
146 | array(
147 | new ControlPanelCheckbox(
148 | /* I18N: Module Configuration */I18N::translate('Restrict to specific facts and events'),
149 | /* I18N: Module Configuration */I18N::translate('If this option is checked, shared place data is only displayed for the following facts and events. ') .
150 | CommonI18N::bothEmpty(),
151 | 'RESTRICTED',
152 | '0'),
153 | ControlPanelFactRestriction::createWithIndividualFacts(
154 | CommonI18N::restrictIndi(),
155 | 'RESTRICTED_INDI',
156 | 'BIRT,OCCU,RESI,DEAT'),
157 | ControlPanelFactRestriction::createWithFamilyFacts(
158 | CommonI18N::restrictFam(),
159 | 'RESTRICTED_FAM',
160 | 'MARR')));
161 |
162 | $factsAndEventsSub[] = new ControlPanelSubsection(
163 | /* I18N: Module Configuration */I18N::translate('Automatically expand shared place data'),
164 | array(new ControlPanelRadioButtons(
165 | false,
166 | array(
167 | new ControlPanelRadioButton(
168 | ' './* I18N: Module Configuration */MoreI18N::xlate('no'),
169 | null,
170 | '0'),
171 | new ControlPanelRadioButton(
172 | /* I18N: Module Configuration */I18N::translate('yes, but only the first occurrence of the shared place'),
173 | /* I18N: Module Configuration */I18N::translate('Note that the first occurrence may be within a toggleable, currently hidden fact or event (such as an event of a close relative). This will probably be improved in future versions of the module.'),
174 | '1'),
175 | new ControlPanelRadioButton(
176 | /* I18N: Module Configuration */MoreI18N::xlate('yes'),
177 | null,
178 | '2')),
179 | null,
180 | 'EXPAND',
181 | '1')));
182 |
183 | $listSub = array();
184 | $listSub[] = new ControlPanelSubsection(
185 | CommonI18N::displayedData(),
186 | array(new ControlPanelCheckbox(
187 | /* I18N: Module Configuration */I18N::translate('Show link counts for shared places list'),
188 | /* I18N: Module Configuration */I18N::translate('Determining the link counts (linked individual/families) is expensive when assigning shared places via name, and therefore causes delays when the shared places list is displayed. It\'s recommended to only select this option if places are assigned via XREFs.'),
189 | 'LINK_COUNTS',
190 | '1')));
191 |
192 | $pageSub = array();
193 | $pageSub[] = new ControlPanelSubsection(
194 | /* I18N: Module Configuration */I18N::translate('Summary'),
195 | array(new ControlPanelTextbox(
196 | /* I18N: Module Configuration */I18N::translate('Reference year'),
197 | /* I18N: Module Configuration */I18N::translate('The year set here may be used by other modules to enhance the place description with additional data. If left empty, the current year is used.'),
198 | 'REF_YEAR',
199 | '',
200 | false,
201 | 4,
202 | '[1-2][0-9][0-9][0-9]')));
203 |
204 | $pageSub[] = new ControlPanelSubsection(
205 | CommonI18N::placeHistory(),
206 | array(
207 | new ControlPanelFactRestriction(
208 | PlaceHistory::getPicklistFacts(),
209 | CommonI18N::restrictPlaceHistory(),
210 | 'RESTRICTED_PLACE_HISTORY',
211 | PlaceHistory::initialFactsStringForPreferences())));
212 |
213 | $hierarchySub = array();
214 | $hierarchySub[] = new ControlPanelSubsection(
215 | CommonI18N::displayedData(),
216 | array(new ControlPanelCheckbox(
217 | /* I18N: Module Configuration */I18N::translate('Filter to unique shared places'),
218 | /* I18N: Module Configuration */I18N::translate('In the place hierarchy list, when using the option \'restrict to shared places\', shared places with multiple names show up multiple times as separate entries. Check this option to show each shared place only once in this case, under the shared place\'s primary name, and also show its additional names.'),
219 | 'UNIQUE_SP_IN_HIERARCHY',
220 | '0')));
221 |
222 | $sections = array();
223 | $sections[] = new ControlPanelSection(
224 | CommonI18N::general(),
225 | null,
226 | $generalSub);
227 |
228 | //TODO: support this again?
229 | if (false) {
230 | $sections[] = new ControlPanelSection(
231 | /* I18N: Module Configuration */I18N::translate('Facts for shared place records'),
232 | null,
233 | $factsSub);
234 | }
235 |
236 | $sections[] = new ControlPanelSection(
237 | CommonI18N::factsAndEventsTabSettings(),
238 | null,
239 | $factsAndEventsSub);
240 | $sections[] = new ControlPanelSection(
241 | /* I18N: Module Configuration */I18N::translate('Shared places list'),
242 | null,
243 | $listSub);
244 | $sections[] = new ControlPanelSection(
245 | /* I18N: Module Configuration */I18N::translate('Shared place page'),
246 | null,
247 | $pageSub);
248 | $sections[] = new ControlPanelSection(
249 | /* I18N: Module Configuration */MoreI18N::xlate('Place hierarchy'),
250 | null,
251 | $hierarchySub);
252 |
253 | return new ControlPanelPreferences($sections);
254 | }
255 |
256 | //TODO: this is webtrees_20!
257 | public static function getPicklistFactsLoc(bool $forRequired = false): array {
258 | $tags = [
259 | "NAME",
260 | "_LOC:TYPE",
261 | "NOTE",
262 | "SHARED_NOTE",
263 | "SOUR",
264 | "_LOC:_LOC",
265 | "MAP",
266 | "_GOV"];
267 |
268 | if ($forRequired) {
269 | //others are redundant, tricky, or anyway TBI
270 | $tags = [
271 | "NOTE"];
272 |
273 | //"SHARED_NOTE" problematic (potential modal within modal)
274 | }
275 |
276 | $facts = [];
277 | foreach ($tags as $tag) {
278 | $facts[$tag] = GedcomTag::getLabel($tag);
279 | }
280 | uasort($facts, '\Fisharebest\Webtrees\I18N::strcasecmp');
281 |
282 | return $facts;
283 | }
284 | }
285 |
--------------------------------------------------------------------------------