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