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