Passed
Pull Request — main (#4307)
by David
06:09
created

PlaceHierarchyListModule::postUpdateDataAction()   A

Complexity

Conditions 5
Paths 4

Size

Total Lines 32
Code Lines 24

Duplication

Lines 0
Ratio 0 %

Importance

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