Passed
Push — develop ( 1125ee...8bca93 )
by Greg
20:39 queued 07:22
created

PlaceHierarchyListModule::individualPlaceLinks()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 7
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 5
nc 1
nop 1
dl 0
loc 7
rs 10
c 0
b 0
f 0
1
<?php
2
3
/**
4
 * webtrees: online genealogy
5
 * Copyright (C) 2022 webtrees development team
6
 * This program is free software: you can redistribute it and/or modify
7
 * it under the terms of the GNU General Public License as published by
8
 * the Free Software Foundation, either version 3 of the License, or
9
 * (at your option) any later version.
10
 * This program is distributed in the hope that it will be useful,
11
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
 * GNU General Public License for more details.
14
 * You should have received a copy of the GNU General Public License
15
 * along with this program. If not, see <https://www.gnu.org/licenses/>.
16
 */
17
18
declare(strict_types=1);
19
20
namespace Fisharebest\Webtrees\Module;
21
22
use Fisharebest\Webtrees\Auth;
23
use Fisharebest\Webtrees\Family;
24
use Fisharebest\Webtrees\I18N;
25
use Fisharebest\Webtrees\Individual;
26
use Fisharebest\Webtrees\Location;
27
use Fisharebest\Webtrees\Place;
28
use Fisharebest\Webtrees\PlaceLocation;
29
use Fisharebest\Webtrees\Registry;
30
use Fisharebest\Webtrees\Services\LeafletJsService;
31
use Fisharebest\Webtrees\Services\ModuleService;
32
use Fisharebest\Webtrees\Services\SearchService;
33
use Fisharebest\Webtrees\Tree;
34
use Fisharebest\Webtrees\Validator;
35
use Illuminate\Database\Capsule\Manager as DB;
36
use Illuminate\Database\Query\Builder;
37
use Illuminate\Database\Query\JoinClause;
38
use Psr\Http\Message\ResponseInterface;
39
use Psr\Http\Message\ServerRequestInterface;
40
use Psr\Http\Server\RequestHandlerInterface;
41
42
use function array_chunk;
43
use function array_pop;
44
use function array_reverse;
45
use function ceil;
46
use function count;
47
use function route;
48
use function view;
49
50
/**
51
 * Class IndividualListModule
52
 */
53
class PlaceHierarchyListModule extends AbstractModule implements ModuleListInterface, RequestHandlerInterface
54
{
55
    use ModuleListTrait;
56
57
    protected const ROUTE_URL = '/tree/{tree}/place-list';
58
59
    /** @var int The default access level for this module.  It can be changed in the control panel. */
60
    protected int $access_level = Auth::PRIV_USER;
61
62
    private LeafletJsService $leaflet_js_service;
63
64
    private ModuleService $module_service;
65
66
    private SearchService $search_service;
67
68
    /**
69
     * PlaceHierarchy constructor.
70
     *
71
     * @param LeafletJsService $leaflet_js_service
72
     * @param ModuleService    $module_service
73
     * @param SearchService    $search_service
74
     */
75
    public function __construct(LeafletJsService $leaflet_js_service, ModuleService $module_service, SearchService $search_service)
76
    {
77
        $this->leaflet_js_service = $leaflet_js_service;
78
        $this->module_service     = $module_service;
79
        $this->search_service     = $search_service;
80
    }
81
82
    /**
83
     * Initialization.
84
     *
85
     * @return void
86
     */
87
    public function boot(): void
88
    {
89
        Registry::routeFactory()->routeMap()
90
            ->get(static::class, static::ROUTE_URL, $this);
91
    }
92
93
    /**
94
     * How should this module be identified in the control panel, etc.?
95
     *
96
     * @return string
97
     */
98
    public function title(): string
99
    {
100
        /* I18N: Name of a module/list */
101
        return I18N::translate('Place hierarchy');
102
    }
103
104
    /**
105
     * A sentence describing what this module does.
106
     *
107
     * @return string
108
     */
109
    public function description(): string
110
    {
111
        /* I18N: Description of the “Place hierarchy” module */
112
        return I18N::translate('The place hierarchy.');
113
    }
114
115
    /**
116
     * CSS class for the URL.
117
     *
118
     * @return string
119
     */
120
    public function listMenuClass(): string
121
    {
122
        return 'menu-list-plac';
123
    }
124
125
    /**
126
     * @return array<string>
127
     */
128
    public function listUrlAttributes(): array
129
    {
130
        return [];
131
    }
132
133
    /**
134
     * @param Tree $tree
135
     *
136
     * @return bool
137
     */
138
    public function listIsEmpty(Tree $tree): bool
139
    {
140
        return !DB::table('places')
141
            ->where('p_file', '=', $tree->id())
142
            ->exists();
143
    }
144
145
    /**
146
     * Handle URLs generated by older versions of webtrees
147
     *
148
     * @param ServerRequestInterface $request
149
     *
150
     * @return ResponseInterface
151
     */
152
    public function getListAction(ServerRequestInterface $request): ResponseInterface
153
    {
154
        $tree = Validator::attributes($request)->tree();
155
156
        return Registry::responseFactory()->redirectUrl($this->listUrl($tree, $request->getQueryParams()));
157
    }
158
159
    /**
160
     * @param Tree                                      $tree
161
     * @param array<bool|int|string|array<string>|null> $parameters
162
     *
163
     * @return string
164
     */
165
    public function listUrl(Tree $tree, array $parameters = []): string
166
    {
167
        $parameters['tree'] = $tree->name();
168
169
        return route(static::class, $parameters);
170
    }
171
172
    /**
173
     * @param ServerRequestInterface $request
174
     *
175
     * @return ResponseInterface
176
     */
177
    public function handle(ServerRequestInterface $request): ResponseInterface
178
    {
179
        $tree = Validator::attributes($request)->tree();
180
        $user = Validator::attributes($request)->user();
181
182
        Auth::checkComponentAccess($this, ModuleListInterface::class, $tree, $user);
183
184
        $action2  = $request->getQueryParams()['action2'] ?? 'hierarchy';
185
        $place_id = (int) ($request->getQueryParams()['place_id'] ?? 0);
186
        $place    = Place::find($place_id, $tree);
187
188
        // Request for a non-existent place?
189
        if ($place_id !== $place->id()) {
190
            return Registry::responseFactory()->redirectUrl($place->url());
191
        }
192
193
        $map_providers = $this->module_service->findByInterface(ModuleMapProviderInterface::class);
194
195
        $content = '';
196
        $showmap = $map_providers->isNotEmpty();
197
        $data    = null;
198
199
        if ($showmap) {
200
            $content .= view('modules/place-hierarchy/map', [
201
                'data'           => $this->mapData($tree, $place),
202
                'leaflet_config' => $this->leaflet_js_service->config(),
203
            ]);
204
        }
205
206
        switch ($action2) {
207
            case 'list':
208
            default:
209
                $alt_link = I18N::translate('Show place hierarchy');
210
                $alt_url  = $this->listUrl($tree, ['action2' => 'hierarchy', 'place_id' => $place_id]);
211
                $content .= view('modules/place-hierarchy/list', ['columns' => $this->getList($tree)]);
212
                break;
213
            case 'hierarchy':
214
            case 'hierarchy-e':
215
                $alt_link = I18N::translate('Show all places in a list');
216
                $alt_url  = $this->listUrl($tree, ['action2' => 'list', 'place_id' => 0]);
217
                $data     = $this->getHierarchy($place);
218
                $content .= ($data === null || $showmap) ? '' : view('place-hierarchy', $data);
219
                if ($data === null || $action2 === 'hierarchy-e') {
220
                    $content .= view('modules/place-hierarchy/events', [
221
                        'indilist' => $this->search_service->searchIndividualsInPlace($place),
222
                        'famlist'  => $this->search_service->searchFamiliesInPlace($place),
223
                        'tree'     => $place->tree(),
224
                    ]);
225
                }
226
        }
227
228
        if ($data !== null && $action2 !== 'hierarchy-e' && $place->gedcomName() !== '') {
229
            $events_link = $this->listUrl($tree, ['action2' => 'hierarchy-e', 'place_id' => $place_id]);
230
        } else {
231
            $events_link = '';
232
        }
233
234
        $breadcrumbs = $this->breadcrumbs($place);
235
236
        return $this->viewResponse('modules/place-hierarchy/page', [
237
            'alt_link'    => $alt_link,
238
            'alt_url'     => $alt_url,
239
            'breadcrumbs' => $breadcrumbs['breadcrumbs'],
240
            'content'     => $content,
241
            'current'     => $breadcrumbs['current'],
242
            'events_link' => $events_link,
243
            'place'       => $place,
244
            'title'       => I18N::translate('Place hierarchy'),
245
            'tree'        => $tree,
246
            'world_url'   => $this->listUrl($tree),
247
        ]);
248
    }
249
250
    /**
251
     * @param Tree  $tree
252
     * @param Place $placeObj
253
     *
254
     * @return array<mixed>
255
     */
256
    protected function mapData(Tree $tree, Place $placeObj): array
257
    {
258
        $places    = $placeObj->getChildPlaces();
259
        $features  = [];
260
        $sidebar   = '';
261
        $show_link = true;
262
263
        if ($places === []) {
264
            $places[]  = $placeObj;
265
            $show_link = false;
266
        }
267
268
        foreach ($places as $id => $place) {
269
            $location = new PlaceLocation($place->gedcomName());
270
271
            if ($location->latitude() === null || $location->longitude() === null) {
272
                $sidebar_class = 'unmapped';
273
            } else {
274
                $sidebar_class = 'mapped';
275
                $features[]    = [
276
                    'type'       => 'Feature',
277
                    'id'         => $id,
278
                    'geometry'   => [
279
                        'type'        => 'Point',
280
                        'coordinates' => [$location->longitude(), $location->latitude()],
281
                    ],
282
                    'properties' => [
283
                        'tooltip' => $place->gedcomName(),
284
                        'popup'   => view('modules/place-hierarchy/popup', [
285
                            'showlink'  => $show_link,
286
                            'place'     => $place,
287
                            'latitude'  => $location->latitude(),
288
                            'longitude' => $location->longitude(),
289
                        ]),
290
                    ],
291
                ];
292
            }
293
294
            $stats = [
295
                Family::RECORD_TYPE     => $this->familyPlaceLinks($place)->count(),
296
                Individual::RECORD_TYPE => $this->individualPlaceLinks($place)->count(),
297
                Location::RECORD_TYPE   => $this->locationPlaceLinks($place)->count(),
298
            ];
299
300
            $sidebar .= view('modules/place-hierarchy/sidebar', [
301
                'showlink'      => $show_link,
302
                'id'            => $id,
303
                'place'         => $place,
304
                'sidebar_class' => $sidebar_class,
305
                'stats'         => $stats,
306
            ]);
307
        }
308
309
        return [
310
            'bounds'  => (new PlaceLocation($placeObj->gedcomName()))->boundingRectangle(),
311
            'sidebar' => $sidebar,
312
            'markers' => [
313
                'type'     => 'FeatureCollection',
314
                'features' => $features,
315
            ],
316
        ];
317
    }
318
319
    /**
320
     * @param Tree $tree
321
     *
322
     * @return array<array<Place>>
323
     */
324
    private function getList(Tree $tree): array
325
    {
326
        $places = $this->search_service->searchPlaces($tree, '')
327
            ->sort(static function (Place $x, Place $y): int {
328
                return $x->gedcomName() <=> $y->gedcomName();
329
            })
330
            ->all();
331
332
        $count = count($places);
333
334
        if ($places === []) {
335
            return [];
336
        }
337
338
        $columns = $count > 20 ? 3 : 2;
339
340
        return array_chunk($places, (int) ceil($count / $columns));
341
    }
342
343
    /**
344
     * @param Place $place
345
     *
346
     * @return array{'tree':Tree,'col_class':string,'columns':array<array<Place>>,'place':Place}|null
347
     */
348
    private function getHierarchy(Place $place): ?array
349
    {
350
        $child_places = $place->getChildPlaces();
351
        $numfound     = count($child_places);
352
353
        if ($numfound > 0) {
354
            $divisor = $numfound > 20 ? 3 : 2;
355
356
            return [
357
                'tree'      => $place->tree(),
358
                'col_class' => 'w-' . ($divisor === 2 ? '25' : '50'),
359
                'columns'   => array_chunk($child_places, (int) ceil($numfound / $divisor)),
360
                'place'     => $place,
361
            ];
362
        }
363
364
        return null;
365
    }
366
367
    /**
368
     * @param Place $place
369
     *
370
     * @return array{'breadcrumbs':array<Place>,'current':Place|null}
371
     */
372
    private function breadcrumbs(Place $place): array
373
    {
374
        $breadcrumbs = [];
375
        if ($place->gedcomName() !== '') {
376
            $breadcrumbs[] = $place;
377
            $parent_place  = $place->parent();
378
            while ($parent_place->gedcomName() !== '') {
379
                $breadcrumbs[] = $parent_place;
380
                $parent_place  = $parent_place->parent();
381
            }
382
            $breadcrumbs = array_reverse($breadcrumbs);
383
            $current     = array_pop($breadcrumbs);
384
        } else {
385
            $current = null;
386
        }
387
388
        return [
389
            'breadcrumbs' => $breadcrumbs,
390
            'current'     => $current,
391
        ];
392
    }
393
394
    /**
395
     * @param Place $place
396
     *
397
     * @return Builder
398
     */
399
    private function placeLinks(Place $place): Builder
400
    {
401
        return DB::table('places')
402
            ->join('placelinks', static function (JoinClause $join): void {
403
                $join
404
                    ->on('pl_file', '=', 'p_file')
405
                    ->on('pl_p_id', '=', 'p_id');
406
            })
407
            ->where('p_file', '=', $place->tree()->id())
408
            ->where('p_id', '=', $place->id());
409
    }
410
411
    /**
412
     * @param Place $place
413
     *
414
     * @return Builder
415
     */
416
    private function familyPlaceLinks(Place $place): Builder
417
    {
418
        return $this->placeLinks($place)
419
            ->join('families', static function (JoinClause $join): void {
420
                $join
421
                    ->on('pl_file', '=', 'f_file')
422
                    ->on('pl_gid', '=', 'f_id');
423
            });
424
    }
425
426
    /**
427
     * @param Place $place
428
     *
429
     * @return Builder
430
     */
431
    private function individualPlaceLinks(Place $place): Builder
432
    {
433
        return $this->placeLinks($place)
434
            ->join('individuals', static function (JoinClause $join): void {
435
                $join
436
                    ->on('pl_file', '=', 'i_file')
437
                    ->on('pl_gid', '=', 'i_id');
438
            });
439
    }
440
441
    /**
442
     * @param Place $place
443
     *
444
     * @return Builder
445
     */
446
    private function locationPlaceLinks(Place $place): Builder
447
    {
448
        return $this->placeLinks($place)
449
            ->join('other', static function (JoinClause $join): void {
450
                $join
451
                    ->on('pl_file', '=', 'o_file')
452
                    ->on('pl_gid', '=', 'o_id');
453
            })
454
            ->where('o_type', '=', Location::RECORD_TYPE);
455
    }
456
}
457