xref: /webtrees/app/Module/PlaceHierarchyListModule.php (revision bac83be95cc96bdac6a0a180473e77cbbad83047)
1<?php
2
3/**
4 * webtrees: online genealogy
5 * Copyright (C) 2020 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 <http://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\ModuleService;
29use Fisharebest\Webtrees\Services\SearchService;
30use Fisharebest\Webtrees\Services\UserService;
31use Fisharebest\Webtrees\Site;
32use Fisharebest\Webtrees\Statistics;
33use Fisharebest\Webtrees\Tree;
34use Fisharebest\Webtrees\Webtrees;
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 is_file;
47use function redirect;
48use function route;
49use function view;
50
51/**
52 * Class IndividualListModule
53 */
54class PlaceHierarchyListModule extends AbstractModule implements ModuleListInterface, RequestHandlerInterface
55{
56    use ModuleListTrait;
57
58    protected const ROUTE_URL = '/tree/{tree}/place-list';
59
60    /** @var int The default access level for this module.  It can be changed in the control panel. */
61    protected $access_level = Auth::PRIV_USER;
62
63    /** @var SearchService */
64    private $search_service;
65
66    /**
67     * PlaceHierarchy constructor.
68     *
69     * @param SearchService $search_service
70     */
71    public function __construct(SearchService $search_service)
72    {
73        $this->search_service = $search_service;
74    }
75
76    /**
77     * Initialization.
78     *
79     * @return void
80     */
81    public function boot(): void
82    {
83        $router_container = app(RouterContainer::class);
84        assert($router_container instanceof RouterContainer);
85
86        $router_container->getMap()
87            ->get(static::class, static::ROUTE_URL, $this);
88    }
89
90    /**
91     * How should this module be identified in the control panel, etc.?
92     *
93     * @return string
94     */
95    public function title(): string
96    {
97        /* I18N: Name of a module/list */
98        return I18N::translate('Place hierarchy');
99    }
100
101    /**
102     * A sentence describing what this module does.
103     *
104     * @return string
105     */
106    public function description(): string
107    {
108        /* I18N: Description of the “Place hierarchy” module */
109        return I18N::translate('The place hierarchy.');
110    }
111
112    /**
113     * CSS class for the URL.
114     *
115     * @return string
116     */
117    public function listMenuClass(): string
118    {
119        return 'menu-list-plac';
120    }
121
122    /**
123     * @param Tree    $tree
124     * @param mixed[] $parameters
125     *
126     * @return string
127     */
128    public function listUrl(Tree $tree, array $parameters = []): string
129    {
130        $parameters['tree'] = $tree->name();
131
132        return route(static::class, $parameters);
133    }
134
135    /**
136     * @return string[]
137     */
138    public function listUrlAttributes(): array
139    {
140        return [];
141    }
142
143    /**
144     * @param Tree $tree
145     *
146     * @return bool
147     */
148    public function listIsEmpty(Tree $tree): bool
149    {
150        return !DB::table('places')
151            ->where('p_file', '=', $tree->id())
152            ->exists();
153    }
154
155    /**
156     * Handle URLs generated by older versions of webtrees
157     *
158     * @param ServerRequestInterface $request
159     *
160     * @return ResponseInterface
161     */
162    public function getListAction(ServerRequestInterface $request): ResponseInterface
163    {
164        return redirect($this->listUrl($request->getAttribute('tree'), $request->getQueryParams()));
165    }
166
167    /**
168     * @param ServerRequestInterface $request
169     *
170     * @return ResponseInterface
171     */
172    public function handle(ServerRequestInterface $request): ResponseInterface
173    {
174        $tree = $request->getAttribute('tree');
175        assert($tree instanceof Tree);
176
177        $user = $request->getAttribute('user');
178        assert($user instanceof UserInterface);
179
180        Auth::checkComponentAccess($this, ModuleListInterface::class, $tree, $user);
181
182        $module   = $request->getAttribute('module');
183        $action   = $request->getAttribute('action');
184        $action2  = $request->getQueryParams()['action2'] ?? 'hierarchy';
185        $place_id = (int) ($request->getQueryParams()['place_id'] ?? 0);
186        $place    = Place::find($place_id, $tree);
187
188        // Request for a non-existent place?
189        if ($place_id !== $place->id()) {
190            return redirect($place->url());
191        }
192
193        $content    = '';
194        $showmap    = Site::getPreference('map-provider') !== '';
195        $data       = null;
196
197        if ($showmap) {
198            $content .= view('modules/place-hierarchy/map', [
199                'data'     => $this->mapData($tree, $place),
200                'provider' => [
201                    'url'    => 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png',
202                    'options' => [
203                        'attribution' => '<a href="https://www.openstreetmap.org/copyright">&copy; OpenStreetMap</a> contributors',
204                        'max_zoom'    => 19
205                    ]
206                ]
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     *
257     * @return array<array<Place>>
258     */
259    private function getList(Tree $tree): array
260    {
261        $places = $this->search_service->searchPlaces($tree, '')
262            ->sort(static function (Place $x, Place $y): int {
263                return $x->gedcomName() <=> $y->gedcomName();
264            })
265            ->all();
266
267        $count = count($places);
268
269        if ($places === []) {
270            return [];
271        }
272
273        $columns = $count > 20 ? 3 : 2;
274
275        return array_chunk($places, (int) ceil($count / $columns));
276    }
277
278
279    /**
280     * @param Place $place
281     *
282     * @return array{'tree':Tree,'col_class':string,'columns':array<array<Place>>,'place':Place}|null
283     */
284    private function getHierarchy(Place $place): ?array
285    {
286        $child_places = $place->getChildPlaces();
287        $numfound     = count($child_places);
288
289        if ($numfound > 0) {
290            $divisor = $numfound > 20 ? 3 : 2;
291
292            return [
293                'tree'      => $place->tree(),
294                'col_class' => 'w-' . ($divisor === 2 ? '25' : '50'),
295                'columns'   => array_chunk($child_places, (int) ceil($numfound / $divisor)),
296                'place'     => $place,
297            ];
298        }
299
300        return null;
301    }
302
303    /**
304     * @param Place $place
305     *
306     * @return array{'breadcrumbs':array<Place>,'current':Place|null}
307     */
308    private function breadcrumbs(Place $place): array
309    {
310        $breadcrumbs = [];
311        if ($place->gedcomName() !== '') {
312            $breadcrumbs[] = $place;
313            $parent_place  = $place->parent();
314            while ($parent_place->gedcomName() !== '') {
315                $breadcrumbs[] = $parent_place;
316                $parent_place  = $parent_place->parent();
317            }
318            $breadcrumbs = array_reverse($breadcrumbs);
319            $current     = array_pop($breadcrumbs);
320        } else {
321            $current = null;
322        }
323
324        return [
325            'breadcrumbs' => $breadcrumbs,
326            'current'     => $current,
327        ];
328    }
329
330    /**
331     * @param Tree  $tree
332     * @param Place $placeObj
333     *
334     * @return array<mixed>
335     */
336    protected function mapData(Tree $tree, Place $placeObj): array
337    {
338        $places    = $placeObj->getChildPlaces();
339        $features  = [];
340        $sidebar   = '';
341        $flag_path = Webtrees::MODULES_DIR . 'openstreetmap/';
342        $show_link = true;
343
344        if ($places === []) {
345            $places[] = $placeObj;
346            $show_link = false;
347        }
348
349        foreach ($places as $id => $place) {
350            $location = new PlaceLocation($place->gedcomName());
351
352            if ($location->icon() !== '' && is_file($flag_path . $location->icon())) {
353                $flag = $flag_path . $location->icon();
354            } else {
355                $flag = '';
356            }
357
358            if ($location->latitude() === 0.0 && $location->longitude() === 0.0) {
359                $sidebar_class = 'unmapped';
360            } else {
361                $sidebar_class = 'mapped';
362                $features[]    = [
363                    'type'       => 'Feature',
364                    'id'         => $id,
365                    'geometry'   => [
366                        'type'        => 'Point',
367                        'coordinates' => [$location->longitude(), $location->latitude()],
368                    ],
369                    'properties' => [
370                        'tooltip' => $place->gedcomName(),
371                        'popup'   => view('modules/place-hierarchy/popup', [
372                            'showlink'  => $show_link,
373                            'flag'      => $flag,
374                            'place'     => $place,
375                            'latitude'  => $location->latitude(),
376                            'longitude' => $location->longitude(),
377                        ]),
378                    ],
379                ];
380            }
381
382            $statistics = new Statistics(app(ModuleService::class), $tree, app(UserService::class));
383
384            //Stats
385            $placeStats = [];
386            foreach (['INDI', 'FAM'] as $type) {
387                $tmp               = $statistics->statsPlaces($type, '', $place->id());
388                $placeStats[$type] = $tmp === [] ? 0 : $tmp[0]->tot;
389            }
390            $sidebar .= view('modules/place-hierarchy/sidebar', [
391                'showlink'      => $show_link,
392                'flag'          => $flag,
393                'id'            => $id,
394                'place'         => $place,
395                'sidebar_class' => $sidebar_class,
396                'stats'         => $placeStats,
397            ]);
398        }
399
400        return [
401            'bounds'  => (new PlaceLocation($placeObj->gedcomName()))->boundingRectangle(),
402            'sidebar' => $sidebar,
403            'markers' => [
404                'type'     => 'FeatureCollection',
405                'features' => $features,
406            ]
407        ];
408    }
409}
410