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