xref: /webtrees/app/Module/PlaceHierarchyListModule.php (revision 3d9e70a53ebdea3db408ada1769711840cc7d14c)
1<?php
2
3/**
4 * webtrees: online genealogy
5 * Copyright (C) 2021 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
18declare(strict_types=1);
19
20namespace Fisharebest\Webtrees\Module;
21
22use Aura\Router\RouterContainer;
23use Fisharebest\Webtrees\Auth;
24use Fisharebest\Webtrees\Contracts\UserInterface;
25use Fisharebest\Webtrees\Family;
26use Fisharebest\Webtrees\I18N;
27use Fisharebest\Webtrees\Individual;
28use Fisharebest\Webtrees\Location;
29use Fisharebest\Webtrees\Place;
30use Fisharebest\Webtrees\PlaceLocation;
31use Fisharebest\Webtrees\Services\LeafletJsService;
32use Fisharebest\Webtrees\Services\ModuleService;
33use Fisharebest\Webtrees\Services\SearchService;
34use Fisharebest\Webtrees\Services\UserService;
35use Fisharebest\Webtrees\Site;
36use Fisharebest\Webtrees\Statistics;
37use Fisharebest\Webtrees\Tree;
38use Illuminate\Database\Capsule\Manager as DB;
39use Psr\Http\Message\ResponseInterface;
40use Psr\Http\Message\ServerRequestInterface;
41use Psr\Http\Server\RequestHandlerInterface;
42
43use function array_chunk;
44use function array_pop;
45use function array_reverse;
46use function assert;
47use function ceil;
48use function count;
49use function redirect;
50use function route;
51use function var_export;
52use function view;
53
54/**
55 * Class IndividualListModule
56 */
57class PlaceHierarchyListModule extends AbstractModule implements ModuleListInterface, RequestHandlerInterface
58{
59    use ModuleListTrait;
60
61    protected const ROUTE_URL = '/tree/{tree}/place-list';
62
63    /** @var int The default access level for this module.  It can be changed in the control panel. */
64    protected $access_level = Auth::PRIV_USER;
65
66    private LeafletJsService $leaflet_js_service;
67
68    private ModuleService $module_service;
69
70    private SearchService $search_service;
71
72    /**
73     * PlaceHierarchy constructor.
74     *
75     * @param LeafletJsService $leaflet_js_service
76     * @param ModuleService    $module_service
77     * @param SearchService    $search_service
78     */
79    public function __construct(LeafletJsService $leaflet_js_service, ModuleService $module_service, SearchService $search_service)
80    {
81        $this->leaflet_js_service = $leaflet_js_service;
82        $this->module_service = $module_service;
83        $this->search_service     = $search_service;
84    }
85
86    /**
87     * Initialization.
88     *
89     * @return void
90     */
91    public function boot(): void
92    {
93        $router_container = app(RouterContainer::class);
94        assert($router_container instanceof RouterContainer);
95
96        $router_container->getMap()
97            ->get(static::class, static::ROUTE_URL, $this);
98    }
99
100    /**
101     * How should this module be identified in the control panel, etc.?
102     *
103     * @return string
104     */
105    public function title(): string
106    {
107        /* I18N: Name of a module/list */
108        return I18N::translate('Place hierarchy');
109    }
110
111    /**
112     * A sentence describing what this module does.
113     *
114     * @return string
115     */
116    public function description(): string
117    {
118        /* I18N: Description of the “Place hierarchy” module */
119        return I18N::translate('The place hierarchy.');
120    }
121
122    /**
123     * CSS class for the URL.
124     *
125     * @return string
126     */
127    public function listMenuClass(): string
128    {
129        return 'menu-list-plac';
130    }
131
132    /**
133     * @return array<string>
134     */
135    public function listUrlAttributes(): array
136    {
137        return [];
138    }
139
140    /**
141     * @param Tree $tree
142     *
143     * @return bool
144     */
145    public function listIsEmpty(Tree $tree): bool
146    {
147        return !DB::table('places')
148            ->where('p_file', '=', $tree->id())
149            ->exists();
150    }
151
152    /**
153     * Handle URLs generated by older versions of webtrees
154     *
155     * @param ServerRequestInterface $request
156     *
157     * @return ResponseInterface
158     */
159    public function getListAction(ServerRequestInterface $request): ResponseInterface
160    {
161        return redirect($this->listUrl($request->getAttribute('tree'), $request->getQueryParams()));
162    }
163
164    /**
165     * @param Tree         $tree
166     * @param array<mixed> $parameters
167     *
168     * @return string
169     */
170    public function listUrl(Tree $tree, array $parameters = []): string
171    {
172        $parameters['tree'] = $tree->name();
173
174        return route(static::class, $parameters);
175    }
176
177    /**
178     * @param ServerRequestInterface $request
179     *
180     * @return ResponseInterface
181     */
182    public function handle(ServerRequestInterface $request): ResponseInterface
183    {
184        $tree = $request->getAttribute('tree');
185        assert($tree instanceof Tree);
186
187        $user = $request->getAttribute('user');
188        assert($user instanceof UserInterface);
189
190        Auth::checkComponentAccess($this, ModuleListInterface::class, $tree, $user);
191
192        $action2  = $request->getQueryParams()['action2'] ?? 'hierarchy';
193        $place_id = (int) ($request->getQueryParams()['place_id'] ?? 0);
194        $place    = Place::find($place_id, $tree);
195
196        // Request for a non-existent place?
197        if ($place_id !== $place->id()) {
198            return redirect($place->url());
199        }
200
201        $map_providers = $this->module_service->findByInterface(ModuleMapProviderInterface::class);
202
203        $content = '';
204        $showmap = $map_providers->isNotEmpty();
205        $data    = null;
206
207        if ($showmap) {
208            $content .= view('modules/place-hierarchy/map', [
209                'data'           => $this->mapData($tree, $place),
210                'leaflet_config' => $this->leaflet_js_service->config(),
211            ]);
212        }
213
214        switch ($action2) {
215            case 'list':
216            default:
217                $alt_link = I18N::translate('Show place hierarchy');
218                $alt_url  = $this->listUrl($tree, ['action2' => 'hierarchy', 'place_id' => $place_id]);
219                $content  .= view('modules/place-hierarchy/list', ['columns' => $this->getList($tree)]);
220                break;
221            case 'hierarchy':
222            case 'hierarchy-e':
223                $alt_link = I18N::translate('Show all places in a list');
224                $alt_url  = $this->listUrl($tree, ['action2' => 'list', 'place_id' => 0]);
225                $data     = $this->getHierarchy($place);
226                $content  .= (null === $data || $showmap) ? '' : view('place-hierarchy', $data);
227                if (null === $data || $action2 === 'hierarchy-e') {
228                    $content .= view('modules/place-hierarchy/events', [
229                        'indilist' => $this->search_service->searchIndividualsInPlace($place),
230                        'famlist'  => $this->search_service->searchFamiliesInPlace($place),
231                        'tree'     => $place->tree(),
232                    ]);
233                }
234        }
235
236        if ($data !== null && $action2 !== 'hierarchy-e' && $place->gedcomName() !== '') {
237            $events_link = $this->listUrl($tree, ['action2' => 'hierarchy-e', 'place_id' => $place_id]);
238        } else {
239            $events_link = '';
240        }
241
242        $breadcrumbs = $this->breadcrumbs($place);
243
244        return $this->viewResponse('modules/place-hierarchy/page', [
245            'alt_link'    => $alt_link,
246            'alt_url'     => $alt_url,
247            'breadcrumbs' => $breadcrumbs['breadcrumbs'],
248            'content'     => $content,
249            'current'     => $breadcrumbs['current'],
250            'events_link' => $events_link,
251            'place'       => $place,
252            'title'       => I18N::translate('Place hierarchy'),
253            'tree'        => $tree,
254            'world_url'   => $this->listUrl($tree),
255        ]);
256    }
257
258    /**
259     * @param Tree  $tree
260     * @param Place $placeObj
261     *
262     * @return array<mixed>
263     */
264    protected function mapData(Tree $tree, Place $placeObj): array
265    {
266        $places    = $placeObj->getChildPlaces();
267        $features  = [];
268        $sidebar   = '';
269        $show_link = true;
270
271        if ($places === []) {
272            $places[]  = $placeObj;
273            $show_link = false;
274        }
275
276        foreach ($places as $id => $place) {
277            $location = new PlaceLocation($place->gedcomName());
278
279            if ($location->latitude() === null || $location->longitude() === null) {
280                $sidebar_class = 'unmapped';
281            } else {
282                $sidebar_class = 'mapped';
283                $features[]    = [
284                    'type'       => 'Feature',
285                    'id'         => $id,
286                    'geometry'   => [
287                        'type'        => 'Point',
288                        'coordinates' => [$location->longitude(), $location->latitude()],
289                    ],
290                    'properties' => [
291                        'tooltip' => $place->gedcomName(),
292                        'popup'   => view('modules/place-hierarchy/popup', [
293                            'showlink'  => $show_link,
294                            'place'     => $place,
295                            'latitude'  => $location->latitude(),
296                            'longitude' => $location->longitude(),
297                        ]),
298                    ],
299                ];
300            }
301
302            $statistics = new Statistics(app(ModuleService::class), $tree, app(UserService::class));
303
304            //Stats
305            $stats = [];
306            foreach ([Individual::RECORD_TYPE, Family::RECORD_TYPE, Location::RECORD_TYPE] as $type) {
307                $tmp          = $statistics->statsPlaces($type, '', $place->id());
308                $stats[$type] = $tmp === [] ? 0 : $tmp[0]->tot;
309            }
310            $sidebar .= view('modules/place-hierarchy/sidebar', [
311                'showlink'      => $show_link,
312                'id'            => $id,
313                'place'         => $place,
314                'sidebar_class' => $sidebar_class,
315                'stats'         => $stats,
316            ]);
317        }
318
319        return [
320            'bounds'  => (new PlaceLocation($placeObj->gedcomName()))->boundingRectangle(),
321            'sidebar' => $sidebar,
322            'markers' => [
323                'type'     => 'FeatureCollection',
324                'features' => $features,
325            ],
326        ];
327    }
328
329    /**
330     * @param Tree $tree
331     *
332     * @return array<array<Place>>
333     */
334    private function getList(Tree $tree): array
335    {
336        $places = $this->search_service->searchPlaces($tree, '')
337            ->sort(static function (Place $x, Place $y): int {
338                return $x->gedcomName() <=> $y->gedcomName();
339            })
340            ->all();
341
342        $count = count($places);
343
344        if ($places === []) {
345            return [];
346        }
347
348        $columns = $count > 20 ? 3 : 2;
349
350        return array_chunk($places, (int) ceil($count / $columns));
351    }
352
353    /**
354     * @param Place $place
355     *
356     * @return array{'tree':Tree,'col_class':string,'columns':array<array<Place>>,'place':Place}|null
357     */
358    private function getHierarchy(Place $place): ?array
359    {
360        $child_places = $place->getChildPlaces();
361        $numfound     = count($child_places);
362
363        if ($numfound > 0) {
364            $divisor = $numfound > 20 ? 3 : 2;
365
366            return [
367                'tree'      => $place->tree(),
368                'col_class' => 'w-' . ($divisor === 2 ? '25' : '50'),
369                'columns'   => array_chunk($child_places, (int) ceil($numfound / $divisor)),
370                'place'     => $place,
371            ];
372        }
373
374        return null;
375    }
376
377    /**
378     * @param Place $place
379     *
380     * @return array{'breadcrumbs':array<Place>,'current':Place|null}
381     */
382    private function breadcrumbs(Place $place): array
383    {
384        $breadcrumbs = [];
385        if ($place->gedcomName() !== '') {
386            $breadcrumbs[] = $place;
387            $parent_place  = $place->parent();
388            while ($parent_place->gedcomName() !== '') {
389                $breadcrumbs[] = $parent_place;
390                $parent_place  = $parent_place->parent();
391            }
392            $breadcrumbs = array_reverse($breadcrumbs);
393            $current     = array_pop($breadcrumbs);
394        } else {
395            $current = null;
396        }
397
398        return [
399            'breadcrumbs' => $breadcrumbs,
400            'current'     => $current,
401        ];
402    }
403}
404