xref: /webtrees/app/Module/PlaceHierarchyListModule.php (revision c9c6f2ec6e88594e58f14a6418e0de5aaa1484bd)
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\I18N;
26use Fisharebest\Webtrees\Place;
27use Fisharebest\Webtrees\PlaceLocation;
28use Fisharebest\Webtrees\Services\LeafletJsService;
29use Fisharebest\Webtrees\Services\ModuleService;
30use Fisharebest\Webtrees\Services\SearchService;
31use Fisharebest\Webtrees\Services\UserService;
32use Fisharebest\Webtrees\Site;
33use Fisharebest\Webtrees\Statistics;
34use Fisharebest\Webtrees\Tree;
35use Illuminate\Database\Capsule\Manager as DB;
36use Psr\Http\Message\ResponseInterface;
37use Psr\Http\Message\ServerRequestInterface;
38use Psr\Http\Server\RequestHandlerInterface;
39
40use function array_chunk;
41use function array_pop;
42use function array_reverse;
43use function assert;
44use function ceil;
45use function count;
46use function redirect;
47use function route;
48use function view;
49
50/**
51 * Class IndividualListModule
52 */
53class 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 $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        $router_container = app(RouterContainer::class);
90        assert($router_container instanceof RouterContainer);
91
92        $router_container->getMap()
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        return redirect($this->listUrl($request->getAttribute('tree'), $request->getQueryParams()));
158    }
159
160    /**
161     * @param Tree         $tree
162     * @param array<mixed> $parameters
163     *
164     * @return string
165     */
166    public function listUrl(Tree $tree, array $parameters = []): string
167    {
168        $parameters['tree'] = $tree->name();
169
170        return route(static::class, $parameters);
171    }
172
173    /**
174     * @param ServerRequestInterface $request
175     *
176     * @return ResponseInterface
177     */
178    public function handle(ServerRequestInterface $request): ResponseInterface
179    {
180        $tree = $request->getAttribute('tree');
181        assert($tree instanceof Tree);
182
183        $user = $request->getAttribute('user');
184        assert($user instanceof UserInterface);
185
186        Auth::checkComponentAccess($this, ModuleListInterface::class, $tree, $user);
187
188        $action2  = $request->getQueryParams()['action2'] ?? 'hierarchy';
189        $place_id = (int) ($request->getQueryParams()['place_id'] ?? 0);
190        $place    = Place::find($place_id, $tree);
191
192        // Request for a non-existent place?
193        if ($place_id !== $place->id()) {
194            return redirect($place->url());
195        }
196
197        $map_providers = $this->module_service->findByInterface(ModuleMapProviderInterface::class);
198
199        $content = '';
200        $showmap = $map_providers->isNotEmpty();
201        $data    = null;
202
203        if ($showmap) {
204            $content .= view('modules/place-hierarchy/map', [
205                'data'           => $this->mapData($tree, $place),
206                'leaflet_config' => $this->leaflet_js_service->config(),
207            ]);
208        }
209
210        switch ($action2) {
211            case 'list':
212            default:
213                $alt_link = I18N::translate('Show place hierarchy');
214                $alt_url  = $this->listUrl($tree, ['action2' => 'hierarchy', 'place_id' => $place_id]);
215                $content  .= view('modules/place-hierarchy/list', ['columns' => $this->getList($tree)]);
216                break;
217            case 'hierarchy':
218            case 'hierarchy-e':
219                $alt_link = I18N::translate('Show all places in a list');
220                $alt_url  = $this->listUrl($tree, ['action2' => 'list', 'place_id' => 0]);
221                $data     = $this->getHierarchy($place);
222                $content  .= (null === $data || $showmap) ? '' : view('place-hierarchy', $data);
223                if (null === $data || $action2 === 'hierarchy-e') {
224                    $content .= view('modules/place-hierarchy/events', [
225                        'indilist' => $this->search_service->searchIndividualsInPlace($place),
226                        'famlist'  => $this->search_service->searchFamiliesInPlace($place),
227                        'tree'     => $place->tree(),
228                    ]);
229                }
230        }
231
232        if ($data !== null && $action2 !== 'hierarchy-e' && $place->gedcomName() !== '') {
233            $events_link = $this->listUrl($tree, ['action2' => 'hierarchy-e', 'place_id' => $place_id]);
234        } else {
235            $events_link = '';
236        }
237
238        $breadcrumbs = $this->breadcrumbs($place);
239
240        return $this->viewResponse('modules/place-hierarchy/page', [
241            'alt_link'    => $alt_link,
242            'alt_url'     => $alt_url,
243            'breadcrumbs' => $breadcrumbs['breadcrumbs'],
244            'content'     => $content,
245            'current'     => $breadcrumbs['current'],
246            'events_link' => $events_link,
247            'place'       => $place,
248            'title'       => I18N::translate('Place hierarchy'),
249            'tree'        => $tree,
250            'world_url'   => $this->listUrl($tree),
251        ]);
252    }
253
254    /**
255     * @param Tree  $tree
256     * @param Place $placeObj
257     *
258     * @return array<mixed>
259     */
260    protected function mapData(Tree $tree, Place $placeObj): array
261    {
262        $places    = $placeObj->getChildPlaces();
263        $features  = [];
264        $sidebar   = '';
265        $show_link = true;
266
267        if ($places === []) {
268            $places[]  = $placeObj;
269            $show_link = false;
270        }
271
272        foreach ($places as $id => $place) {
273            $location = new PlaceLocation($place->gedcomName());
274
275            if ($location->latitude() === null || $location->longitude() === null) {
276                $sidebar_class = 'unmapped';
277            } else {
278                $sidebar_class = 'mapped';
279                $features[]    = [
280                    'type'       => 'Feature',
281                    'id'         => $id,
282                    'geometry'   => [
283                        'type'        => 'Point',
284                        'coordinates' => [$location->longitude(), $location->latitude()],
285                    ],
286                    'properties' => [
287                        'tooltip' => $place->gedcomName(),
288                        'popup'   => view('modules/place-hierarchy/popup', [
289                            'showlink'  => $show_link,
290                            'place'     => $place,
291                            'latitude'  => $location->latitude(),
292                            'longitude' => $location->longitude(),
293                        ]),
294                    ],
295                ];
296            }
297
298            $statistics = new Statistics(app(ModuleService::class), $tree, app(UserService::class));
299
300            //Stats
301            $placeStats = [];
302            foreach (['INDI', 'FAM'] as $type) {
303                $tmp               = $statistics->statsPlaces($type, '', $place->id());
304                $placeStats[$type] = $tmp === [] ? 0 : $tmp[0]->tot;
305            }
306            $sidebar .= view('modules/place-hierarchy/sidebar', [
307                'showlink'      => $show_link,
308                'id'            => $id,
309                'place'         => $place,
310                'sidebar_class' => $sidebar_class,
311                'stats'         => $placeStats,
312            ]);
313        }
314
315        return [
316            'bounds'  => (new PlaceLocation($placeObj->gedcomName()))->boundingRectangle(),
317            'sidebar' => $sidebar,
318            'markers' => [
319                'type'     => 'FeatureCollection',
320                'features' => $features,
321            ],
322        ];
323    }
324
325    /**
326     * @param Tree $tree
327     *
328     * @return array<array<Place>>
329     */
330    private function getList(Tree $tree): array
331    {
332        $places = $this->search_service->searchPlaces($tree, '')
333            ->sort(static function (Place $x, Place $y): int {
334                return $x->gedcomName() <=> $y->gedcomName();
335            })
336            ->all();
337
338        $count = count($places);
339
340        if ($places === []) {
341            return [];
342        }
343
344        $columns = $count > 20 ? 3 : 2;
345
346        return array_chunk($places, (int) ceil($count / $columns));
347    }
348
349    /**
350     * @param Place $place
351     *
352     * @return array{'tree':Tree,'col_class':string,'columns':array<array<Place>>,'place':Place}|null
353     */
354    private function getHierarchy(Place $place): ?array
355    {
356        $child_places = $place->getChildPlaces();
357        $numfound     = count($child_places);
358
359        if ($numfound > 0) {
360            $divisor = $numfound > 20 ? 3 : 2;
361
362            return [
363                'tree'      => $place->tree(),
364                'col_class' => 'w-' . ($divisor === 2 ? '25' : '50'),
365                'columns'   => array_chunk($child_places, (int) ceil($numfound / $divisor)),
366                'place'     => $place,
367            ];
368        }
369
370        return null;
371    }
372
373    /**
374     * @param Place $place
375     *
376     * @return array{'breadcrumbs':array<Place>,'current':Place|null}
377     */
378    private function breadcrumbs(Place $place): array
379    {
380        $breadcrumbs = [];
381        if ($place->gedcomName() !== '') {
382            $breadcrumbs[] = $place;
383            $parent_place  = $place->parent();
384            while ($parent_place->gedcomName() !== '') {
385                $breadcrumbs[] = $parent_place;
386                $parent_place  = $parent_place->parent();
387            }
388            $breadcrumbs = array_reverse($breadcrumbs);
389            $current     = array_pop($breadcrumbs);
390        } else {
391            $current = null;
392        }
393
394        return [
395            'breadcrumbs' => $breadcrumbs,
396            'current'     => $current,
397        ];
398    }
399}
400