Passed
Push — main ( f783aa...748dbe )
by Greg
06:01
created

PlaceHierarchyListModule   A

Complexity

Total Complexity 40

Size/Duplication

Total Lines 387
Duplicated Lines 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 168
dl 0
loc 387
rs 9.2
c 1
b 0
f 0
wmc 40

17 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 5 1
A description() 0 4 1
A listMenuClass() 0 3 1
A title() 0 4 1
A listUrlAttributes() 0 3 1
A listIsEmpty() 0 5 1
A boot() 0 4 1
A familyPlaceLinks() 0 7 1
A individualPlaceLinks() 0 7 1
A placeLinks() 0 10 1
A locationPlaceLinks() 0 9 1
A listUrl() 0 5 1
C handle() 0 70 13
A getList() 0 17 3
A getHierarchy() 0 17 4
A breadcrumbs() 0 19 3
B mapData() 0 59 5

How to fix   Complexity   

Complex Class

Complex classes like PlaceHierarchyListModule often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use PlaceHierarchyListModule, and based on these observations, apply Extract Interface, too.

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 redirect;
48
use function route;
49
use function view;
50
51
/**
52
 * Class IndividualListModule
53
 */
54
class PlaceHierarchyListModule extends AbstractModule implements ModuleListInterface, RequestHandlerInterface
55
{
56
    use ModuleListTrait;
57
58
    protected const ROUTE_URL = '/tree/{tree}/place-list';
59
60
    /** @var int The default access level for this module.  It can be changed in the control panel. */
61
    protected int $access_level = Auth::PRIV_USER;
62
63
    private LeafletJsService $leaflet_js_service;
64
65
    private ModuleService $module_service;
66
67
    private SearchService $search_service;
68
69
    /**
70
     * PlaceHierarchy constructor.
71
     *
72
     * @param LeafletJsService $leaflet_js_service
73
     * @param ModuleService    $module_service
74
     * @param SearchService    $search_service
75
     */
76
    public function __construct(LeafletJsService $leaflet_js_service, ModuleService $module_service, SearchService $search_service)
77
    {
78
        $this->leaflet_js_service = $leaflet_js_service;
79
        $this->module_service     = $module_service;
80
        $this->search_service     = $search_service;
81
    }
82
83
    /**
84
     * Initialization.
85
     *
86
     * @return void
87
     */
88
    public function boot(): void
89
    {
90
        Registry::routeFactory()->routeMap()
91
            ->get(static::class, static::ROUTE_URL, $this);
92
    }
93
94
    /**
95
     * How should this module be identified in the control panel, etc.?
96
     *
97
     * @return string
98
     */
99
    public function title(): string
100
    {
101
        /* I18N: Name of a module/list */
102
        return I18N::translate('Place hierarchy');
103
    }
104
105
    /**
106
     * A sentence describing what this module does.
107
     *
108
     * @return string
109
     */
110
    public function description(): string
111
    {
112
        /* I18N: Description of the “Place hierarchy” module */
113
        return I18N::translate('The place hierarchy.');
114
    }
115
116
    /**
117
     * CSS class for the URL.
118
     *
119
     * @return string
120
     */
121
    public function listMenuClass(): string
122
    {
123
        return 'menu-list-plac';
124
    }
125
126
    /**
127
     * @return array<string>
128
     */
129
    public function listUrlAttributes(): array
130
    {
131
        return [];
132
    }
133
134
    /**
135
     * @param Tree $tree
136
     *
137
     * @return bool
138
     */
139
    public function listIsEmpty(Tree $tree): bool
140
    {
141
        return !DB::table('places')
142
            ->where('p_file', '=', $tree->id())
143
            ->exists();
144
    }
145
146
    /**
147
     * @param Tree                                      $tree
148
     * @param array<bool|int|string|array<string>|null> $parameters
149
     *
150
     * @return string
151
     */
152
    public function listUrl(Tree $tree, array $parameters = []): string
153
    {
154
        $parameters['tree'] = $tree->name();
155
156
        return route(static::class, $parameters);
157
    }
158
159
    /**
160
     * @param ServerRequestInterface $request
161
     *
162
     * @return ResponseInterface
163
     */
164
    public function handle(ServerRequestInterface $request): ResponseInterface
165
    {
166
        $tree = Validator::attributes($request)->tree();
167
        $user = Validator::attributes($request)->user();
168
169
        Auth::checkComponentAccess($this, ModuleListInterface::class, $tree, $user);
170
171
        $action2  = Validator::queryParams($request)->string('action2', 'hierarchy');
172
        $place_id = Validator::queryParams($request)->integer('place_id', 0);
173
        $place    = Place::find($place_id, $tree);
174
175
        // Request for a non-existent place?
176
        if ($place_id !== $place->id()) {
177
            return redirect($place->url());
178
        }
179
180
        $map_providers = $this->module_service->findByInterface(ModuleMapProviderInterface::class);
181
182
        $content = '';
183
        $showmap = $map_providers->isNotEmpty();
184
        $data    = null;
185
186
        if ($showmap) {
187
            $content .= view('modules/place-hierarchy/map', [
188
                'data'           => $this->mapData($place),
189
                'leaflet_config' => $this->leaflet_js_service->config(),
190
            ]);
191
        }
192
193
        switch ($action2) {
194
            case 'list':
195
            default:
196
                $alt_link = I18N::translate('Show place hierarchy');
197
                $alt_url  = $this->listUrl($tree, ['action2' => 'hierarchy', 'place_id' => $place_id]);
198
                $content .= view('modules/place-hierarchy/list', ['columns' => $this->getList($tree)]);
199
                break;
200
            case 'hierarchy':
201
            case 'hierarchy-e':
202
                $alt_link = I18N::translate('Show all places in a list');
203
                $alt_url  = $this->listUrl($tree, ['action2' => 'list', 'place_id' => 0]);
204
                $data     = $this->getHierarchy($place);
205
                $content .= ($data === null || $showmap) ? '' : view('place-hierarchy', $data);
206
                if ($data === null || $action2 === 'hierarchy-e') {
207
                    $content .= view('modules/place-hierarchy/events', [
208
                        'indilist' => $this->search_service->searchIndividualsInPlace($place),
209
                        'famlist'  => $this->search_service->searchFamiliesInPlace($place),
210
                        'tree'     => $place->tree(),
211
                    ]);
212
                }
213
        }
214
215
        if ($data !== null && $action2 !== 'hierarchy-e' && $place->gedcomName() !== '') {
216
            $events_link = $this->listUrl($tree, ['action2' => 'hierarchy-e', 'place_id' => $place_id]);
217
        } else {
218
            $events_link = '';
219
        }
220
221
        $breadcrumbs = $this->breadcrumbs($place);
222
223
        return $this->viewResponse('modules/place-hierarchy/page', [
224
            'alt_link'    => $alt_link,
225
            'alt_url'     => $alt_url,
226
            'breadcrumbs' => $breadcrumbs['breadcrumbs'],
227
            'content'     => $content,
228
            'current'     => $breadcrumbs['current'],
229
            'events_link' => $events_link,
230
            'place'       => $place,
231
            'title'       => I18N::translate('Place hierarchy'),
232
            'tree'        => $tree,
233
            'world_url'   => $this->listUrl($tree),
234
        ]);
235
    }
236
237
    /**
238
     * @param Place $placeObj
239
     *
240
     * @return array<mixed>
241
     */
242
    protected function mapData(Place $placeObj): array
243
    {
244
        $places    = $placeObj->getChildPlaces();
245
        $features  = [];
246
        $sidebar   = '';
247
        $show_link = true;
248
249
        if ($places === []) {
250
            $places[]  = $placeObj;
251
            $show_link = false;
252
        }
253
254
        foreach ($places as $id => $place) {
255
            $location = new PlaceLocation($place->gedcomName());
256
257
            if ($location->latitude() === null || $location->longitude() === null) {
258
                $sidebar_class = 'unmapped';
259
            } else {
260
                $sidebar_class = 'mapped';
261
                $features[]    = [
262
                    'type'       => 'Feature',
263
                    'id'         => $id,
264
                    'geometry'   => [
265
                        'type'        => 'Point',
266
                        'coordinates' => [$location->longitude(), $location->latitude()],
267
                    ],
268
                    'properties' => [
269
                        'tooltip' => $place->gedcomName(),
270
                        'popup'   => view('modules/place-hierarchy/popup', [
271
                            'showlink'  => $show_link,
272
                            'place'     => $place,
273
                            'latitude'  => $location->latitude(),
274
                            'longitude' => $location->longitude(),
275
                        ]),
276
                    ],
277
                ];
278
            }
279
280
            $stats = [
281
                Family::RECORD_TYPE     => $this->familyPlaceLinks($place)->count(),
282
                Individual::RECORD_TYPE => $this->individualPlaceLinks($place)->count(),
283
                Location::RECORD_TYPE   => $this->locationPlaceLinks($place)->count(),
284
            ];
285
286
            $sidebar .= view('modules/place-hierarchy/sidebar', [
287
                'showlink'      => $show_link,
288
                'id'            => $id,
289
                'place'         => $place,
290
                'sidebar_class' => $sidebar_class,
291
                'stats'         => $stats,
292
            ]);
293
        }
294
295
        return [
296
            'bounds'  => (new PlaceLocation($placeObj->gedcomName()))->boundingRectangle(),
297
            'sidebar' => $sidebar,
298
            'markers' => [
299
                'type'     => 'FeatureCollection',
300
                'features' => $features,
301
            ],
302
        ];
303
    }
304
305
    /**
306
     * @param Tree $tree
307
     *
308
     * @return array<array<Place>>
309
     */
310
    private function getList(Tree $tree): array
311
    {
312
        $places = $this->search_service->searchPlaces($tree, '')
313
            ->sort(static function (Place $x, Place $y): int {
314
                return $x->gedcomName() <=> $y->gedcomName();
315
            })
316
            ->all();
317
318
        $count = count($places);
319
320
        if ($places === []) {
321
            return [];
322
        }
323
324
        $columns = $count > 20 ? 3 : 2;
325
326
        return array_chunk($places, (int) ceil($count / $columns));
327
    }
328
329
    /**
330
     * @param Place $place
331
     *
332
     * @return array{columns:array<array<Place>>,place:Place,tree:Tree,col_class:string}|null
333
     */
334
    private function getHierarchy(Place $place): ?array
335
    {
336
        $child_places = $place->getChildPlaces();
337
        $numfound     = count($child_places);
338
339
        if ($numfound > 0) {
340
            $divisor = $numfound > 20 ? 3 : 2;
341
342
            return [
343
                'tree'      => $place->tree(),
344
                'col_class' => 'w-' . ($divisor === 2 ? '25' : '50'),
345
                'columns'   => array_chunk($child_places, (int) ceil($numfound / $divisor)),
346
                'place'     => $place,
347
            ];
348
        }
349
350
        return null;
351
    }
352
353
    /**
354
     * @param Place $place
355
     *
356
     * @return array{breadcrumbs:array<Place>,current:Place|null}
357
     */
358
    private function breadcrumbs(Place $place): array
359
    {
360
        $breadcrumbs = [];
361
        if ($place->gedcomName() !== '') {
362
            $breadcrumbs[] = $place;
363
            $parent_place  = $place->parent();
364
            while ($parent_place->gedcomName() !== '') {
365
                $breadcrumbs[] = $parent_place;
366
                $parent_place  = $parent_place->parent();
367
            }
368
            $breadcrumbs = array_reverse($breadcrumbs);
369
            $current     = array_pop($breadcrumbs);
370
        } else {
371
            $current = null;
372
        }
373
374
        return [
375
            'breadcrumbs' => $breadcrumbs,
376
            'current'     => $current,
377
        ];
378
    }
379
380
    /**
381
     * @param Place $place
382
     *
383
     * @return Builder
384
     */
385
    private function placeLinks(Place $place): Builder
386
    {
387
        return DB::table('places')
388
            ->join('placelinks', static function (JoinClause $join): void {
389
                $join
390
                    ->on('pl_file', '=', 'p_file')
391
                    ->on('pl_p_id', '=', 'p_id');
392
            })
393
            ->where('p_file', '=', $place->tree()->id())
394
            ->where('p_id', '=', $place->id());
395
    }
396
397
    /**
398
     * @param Place $place
399
     *
400
     * @return Builder
401
     */
402
    private function familyPlaceLinks(Place $place): Builder
403
    {
404
        return $this->placeLinks($place)
405
            ->join('families', static function (JoinClause $join): void {
406
                $join
407
                    ->on('pl_file', '=', 'f_file')
408
                    ->on('pl_gid', '=', 'f_id');
409
            });
410
    }
411
412
    /**
413
     * @param Place $place
414
     *
415
     * @return Builder
416
     */
417
    private function individualPlaceLinks(Place $place): Builder
418
    {
419
        return $this->placeLinks($place)
420
            ->join('individuals', static function (JoinClause $join): void {
421
                $join
422
                    ->on('pl_file', '=', 'i_file')
423
                    ->on('pl_gid', '=', 'i_id');
424
            });
425
    }
426
427
    /**
428
     * @param Place $place
429
     *
430
     * @return Builder
431
     */
432
    private function locationPlaceLinks(Place $place): Builder
433
    {
434
        return $this->placeLinks($place)
435
            ->join('other', static function (JoinClause $join): void {
436
                $join
437
                    ->on('pl_file', '=', 'o_file')
438
                    ->on('pl_gid', '=', 'o_id');
439
            })
440
            ->where('o_type', '=', Location::RECORD_TYPE);
441
    }
442
}
443